# 054. 基于 nginx + lua + java 完成多级缓存架构的核心业务逻辑(一)
本节讲解如何自 nginx 中写 lua 脚本把缓存放到 nginx 本地缓存中
脚本思路:
应用 nginx 的 lua 脚本接收到请求
获取请求参数中的商品 id,以及商品店铺 id
根据商品 id 和商品店铺 id,在 nginx 本地缓存中尝试获取数据
如果在 nginx 本地缓存中没有获取到数据,那么就到 redis 分布式缓存中获取数据,如果获取到了数据,还要设置到 nginx 本地缓存中
但是这里有个问题,建议不要用 nginx+lua 直接去获取 redis 数据
因为 openresty 没有太好的 redis cluster 的支持包,所以建议是发送 http 请求到缓存数据生产服务,由该服务提供一个 http 接口
缓存数生产服务可以基于 redis cluster api 从 redis 中直接获取数据,并返回给 nginx
如果缓存数据生产服务没有在 redis 分布式缓存中没有获取到数据,那么就在自己本地 ehcache 中获取数据,返回数据给 nginx,也要设置到 nginx 本地缓存中
如果 ehcache 本地缓存都没有数据,那么就需要去原始的服务中拉取数据,该服务会从 mysql 中查询,拉取到数据之后,返回给 nginx,并重新设置到 ehcache和 redis 中
这里先不考虑并发问题,后面要专门讲解一套分布式缓存重建并发冲突的问题和解决方案
nginx 最终利用获取到的数据,动态渲染网页模板
将渲染后的网页模板作为 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;
}
}
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()
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
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;
}
}
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)
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/>
2
3
4
5
6
7
8
9
10
11
配置完成后,记得测试配置文件和重启 nginx
/usr/servers/nginx/sbin/nginx -t
/usr/servers/nginx/sbin/nginx -s reload
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;;";
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"
2
3
4
5
6
7
8
看到去访问后端服务了,没有返回信息。下一章继续后端服务的编写
TIP
由于搬家,宿主机 IP 变更了,但是虚拟机上已经安装了好多软件。 没有耐心再修改一次 IP 了,所以改用了 hostonly 来保证虚拟机和宿主机的联通,并且虚拟机可以上网。设置方法在这篇文章中;
所以后续对宿主机的访问,会更改 IP ,与这之前的笔记中看到的 IP 会不一致