# 054. 基于 nginx + lua + java 完成多级缓存架构的核心业务逻辑(一)

本节讲解如何自 nginx 中写 lua 脚本把缓存放到 nginx 本地缓存中

脚本思路:

  1. 应用 nginx 的 lua 脚本接收到请求

  2. 获取请求参数中的商品 id,以及商品店铺 id

  3. 根据商品 id 和商品店铺 id,在 nginx 本地缓存中尝试获取数据

  4. 如果在 nginx 本地缓存中没有获取到数据,那么就到 redis 分布式缓存中获取数据,如果获取到了数据,还要设置到 nginx 本地缓存中

    但是这里有个问题,建议不要用 nginx+lua 直接去获取 redis 数据

    因为 openresty 没有太好的 redis cluster 的支持包,所以建议是发送 http 请求到缓存数据生产服务,由该服务提供一个 http 接口

    缓存数生产服务可以基于 redis cluster api 从 redis 中直接获取数据,并返回给 nginx

  5. 如果缓存数据生产服务没有在 redis 分布式缓存中没有获取到数据,那么就在自己本地 ehcache 中获取数据,返回数据给 nginx,也要设置到 nginx 本地缓存中

  6. 如果 ehcache 本地缓存都没有数据,那么就需要去原始的服务中拉取数据,该服务会从 mysql 中查询,拉取到数据之后,返回给 nginx,并重新设置到 ehcache和 redis 中

    这里先不考虑并发问题,后面要专门讲解一套分布式缓存重建并发冲突的问题和解决方案

  7. nginx 最终利用获取到的数据,动态渲染网页模板

  8. 将渲染后的网页模板作为 http 响应,返回给分发层 nginx

下面来一步一步做;

# 分发层 lua

eshop-cache03 服务器上

/usr/hello/hello.conf

server {
    listen       80;
    server_name  _;
    location /lua {
      default_type 'text/html';
      # 防止响应中文乱码
      charset utf-8;
      content_by_lua_file /usr/hello/lua/hello.lua;
    }

    # 分发逻辑脚本映射
    location /product {
      default_type 'text/html';
      # 防止响应中文乱码
      charset utf-8;
      content_by_lua_file /usr/hello/lua/distribute.lua;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/usr/hello/lua/distribute.lua,这里使用之前写好的分发逻辑修改, 因为想在一个映射中写完商品和店铺信息的分发,所以这里还需要添加一个 shopId

local uri_args = ngx.req.get_uri_args()
-- 获取参数
local productId = uri_args["productId"]
local shopId = uri_args["shopId"]

-- 定义后端应用 ip
local host = {"192.168.99.170", "192.168.99.171"}
-- 对商品 id 取模并计算 hash 值
local hash = ngx.crc32_long(productId)
hash = (hash % 2) + 1
-- 拼接 http 前缀
backend = "http://"..host[hash]

-- 获取到参数中的路径,比如你要访问 /hello,这个例子中是需要传递访问路径的
local method = uri_args["method"]
-- 拼接具体的访问地址不带 host,如:/hello?productId=1
local requestBody = "/"..method.."?productId="..productId.."&shopId="..shopId

-- 获取 http 包
local http = require("resty.http")
local httpc = http.new()

-- 访问,这里有疑问:万一有 cooke 这些脚本支持吗?会很麻烦吗?
local resp, err = httpc:request_uri(backend, {
    method = "GET",
    path = requestBody,
    keepalive=false
})

-- 如果没有响应则输出一个 err 信息
if not resp then
    ngx.say("request error :", err)
    return
end

-- 有响应测输出响应信息
ngx.say(resp.body)  

-- 关闭 http 客户端实例
httpc:close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 应用层 nginx

应用层在 eshop-cache01 和 eshop-cache02 上。 这里可以再 01 上写完逻辑,然后再 copy 到 02 上。

先安装依赖:

# 需要再后端服务获取信息,安装 http 依赖
cd /usr/hello/lualib/resty/
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua  
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua

# 拿到数据之后需要进行模板渲染,添加 template 依赖
# 这里渲染也是使用 lua 来完成
cd /usr/hello/lualib/resty/
wget https://raw.githubusercontent.com/bungle/lua-resty-template/master/lib/resty/template.lua
mkdir /usr/hello/lualib/resty/html
cd /usr/hello/lualib/resty/html
wget https://raw.githubusercontent.com/bungle/lua-resty-template/master/lib/resty/template/html.lua
1
2
3
4
5
6
7
8
9
10
11
12

/usr/hello/hello.conf

# 配置 lua 的一个缓存实例,my_cache 是我们自定义的一块缓存名称
# 要配置在 http 中,server 外,否则会报错
# nginx: [emerg] "lua_shared_dict" directive is not allowed here in /usr/hello/hello.conf:11
lua_shared_dict my_cache 128m;
server {  
    listen       80;  
    server_name  _;

    # 配置模板路径
    set $template_location "/templates";  
    # 当然这个路径需要存在,因为后续需要用来存放 html
    set $template_root "/usr/hello/templates";

    location /lua {  
      default_type 'text/html';  
      # 防止响应中文乱码
      charset utf-8;
      content_by_lua_file /usr/hello/lua/hello.lua;
    }

    # 配置一个脚本映射,访问 product 的时候
    # 就执行 product.lua 脚本来完成 获取缓存渲染 html 并返回 html 的功能
    location /product {
      default_type 'text/html';
      # 防止响应中文乱码
      charset utf-8;
      content_by_lua_file /usr/hello/lua/product.lua;
    }    

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

/usr/hello/lua/product.lua

local uri_args = ngx.req.get_uri_args()
local productId = uri_args["productId"]
local shopId = uri_args["shopId"]

-- 获取到之前配置中分配的缓存对象
local cache_ngx = ngx.shared.my_cache

-- 拼接两个缓存 key
local productCacheKey = "product_info_"..productId
local shopCacheKey = "shop_info_"..shopId

-- 通过缓存对象获取缓存中的 value
local productCache = cache_ngx:get(productCacheKey)
local shopCache = cache_ngx:get(shopCacheKey)

-- 如果缓存中不存在对于的 value
-- 就走后端缓存服务获取数据(缓存服务先走 redis ,不存在再走 ehcache,再走数据库)
if productCache == "" or productCache == nil then
	local http = require("resty.http")
	local httpc = http.new()
  -- 这里地址是开发机器 ip,因为我们在 windows 上开发的,
  -- 这里直接访问开发环境比较方便
	local resp, err = httpc:request_uri("http://192.168.99.111:6002",{
  		method = "GET",
  		path = "/getProductInfo?productId="..productId,
      keepalive=false
	})

	productCache = resp.body
  -- 获取到之后,再设置到缓存中
	cache_ngx:set(productCacheKey, productCache, 10 * 60)
end

if shopCache == "" or shopCache == nil then
	local http = require("resty.http")
	local httpc = http.new()

	local resp, err = httpc:request_uri("http://192.168.99.111:6002",{
  		method = "GET",
  		path = "/getShopInfo?shopId="..shopId,
      keepalive=false
	})

	shopCache = resp.body
	cache_ngx:set(shopCacheKey, shopCache, 10 * 60)
end

-- 因为存到缓存中是一个字符串
-- 所以使用 cjson 库把字符串转成 json 对象
local cjson = require("cjson")
local productCacheJSON = cjson.decode(productCache)
local shopCacheJSON = cjson.decode(shopCache)

-- 把商品信息和店铺信息拼接到一个大 json 对象中
-- 这样做的原因是:template 渲染需要这样做
local context = {
	productId = productCacheJSON.id,
	productName = productCacheJSON.name,
	productPrice = productCacheJSON.price,
	productPictureList = productCacheJSON.pictureList,
	productSpecification = productCacheJSON.specification,
	productService = productCacheJSON.service,
	productColor = productCacheJSON.color,
	productSize = productCacheJSON.size,
	shopId = shopCacheJSON.id,
	shopName = shopCacheJSON.name,
	shopLevel = shopCacheJSON.level,
	shopGoodCommentRate = shopCacheJSON.goodCommentRate
}

-- 使用 template 渲染 product.html 模板
local template = require("resty.template")
template.render("product.html", context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

product.html 内容,就是很简单的插值占位

product id: {* productId *}<br/>
product name: {* productName *}<br/>
product picture list: {* productPictureList *}<br/>
product specification: {* productSpecification *}<br/>
product service: {* productService *}<br/>
product color: {* productColor *}<br/>
product size: {* productSize *}<br/>
shop id: {* shopId *}<br/>
shop name: {* shopName *}<br/>
shop level: {* shopLevel *}<br/>
shop good cooment rate: {* shopGoodCommentRate *}<br/>
1
2
3
4
5
6
7
8
9
10
11

配置完成后,记得测试配置文件和重启 nginx

/usr/servers/nginx/sbin/nginx -t
/usr/servers/nginx/sbin/nginx -s reload
1
2

# 错误解决

如果报错 product.lua:20: module 'resty.http' not found: 那么请检查

vi /usr/servers/nginx/conf/nginx.conf
这里引入的内容是否是 hello 目录下的。
http {
    lua_package_path "/usr/hello/lualib/?.lua;;";
    lua_package_cpath "/usr/hello/lualib/?.so;;";
1
2
3
4
5

# 测试

访问地址:http://eshop-cache02/product?productId=1&shopId=1

肯定会报错,因为后端服务都没有写的。但是可以看看日志报错信息

tail -f /usr/servers/nginx/logs/error.log
可以看到如下的错误:

2019/05/06 22:19:10 [error] 8758#0: *34 connect() failed (113: No route to host), client: 192.168.99.1, server: _, request: "GET /product?productId=1&shopId=1 HTTP/1.1", host: "eshop-cache02"
2019/05/06 22:19:10 [error] 8758#0: *34 lua entry thread aborted: runtime error: /usr/hello/lua/product.lua:29: attempt to index local 'resp' (a nil value)
stack traceback:
coroutine 0:
	/usr/hello/lua/product.lua: in function </usr/hello/lua/product.lua:1>, client: 192.168.99.1, server: _, request: "GET /product?productId=1&shopId=1 HTTP/1.1", host: "eshop-cache02"
1
2
3
4
5
6
7
8

看到去访问后端服务了,没有返回信息。下一章继续后端服务的编写

TIP

由于搬家,宿主机 IP 变更了,但是虚拟机上已经安装了好多软件。 没有耐心再修改一次 IP 了,所以改用了 hostonly 来保证虚拟机和宿主机的联通,并且虚拟机可以上网。设置方法在这篇文章中

所以后续对宿主机的访问,会更改 IP ,与这之前的笔记中看到的 IP 会不一致