文章目录
- 前言
- 6.5 实现多级缓存
- 6.5.3 请求参数处理
- 6.5.3.1 获取参数API
- 6.5.3.2 获取参数并返回
- 6.5.4 查询Tomcat
- 6.5.4.1 发送HTTP请求的API
- 6.5.4.2 封装HTTP工具
- 6.5.4.3 实现商品查询
- 6.5.4.4 使用CJSON工具类
- 6.5.4.5 基于商品ID实现负载均衡
- 6.5.5 查询Redis
- 6.5.5.1 Redis缓存预热
- 6.5.5.2 封装Redis工具
- 6.5.5.3 实现Redis查询
- 6.5.5.4 功能测试
前言
Redis多级缓存系列文章:
Redis从入门到精通(十六)多级缓存(一)Caffeine、JVM进程缓存
Redis从入门到精通(十七)多级缓存(二)Lua语言入门、OpenResty集群的安装与使用
6.5 实现多级缓存
6.5.3 请求参数处理
上一节中,OpenResty集群接收前端请求,但是返回的是假数据。而要返回真实数据,必须根据前端传递来的商品ID,查询商品信息才可以。
6.5.3.1 获取参数API
OpenResty提供了一系列API来获取不同类型的前端请求参数:
6.5.3.2 获取参数并返回
在测试项目中,根据ID查询商品信息的请求是:/api/item/1
,可见商品ID是以路径占位符的方式传递的,因此可以利用正则表达式匹配的方式来获取ID。
- 1)获取商品ID
修改/usr/loca/openresty/nginx/nginx.conf
文件中监听/api/item
的代码,利用正则表达式获取商品ID:
location /api/item/(\d+) {
# 默认的响应类型
default_type application/json;
# 响应结果由lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
- 2)拼接ID并返回
修改/usr/loca/openresty/nginx/lua/item.lua
文件,获取商品ID并拼接到结果中返回:
-- 获取商品ID
local id = ngx.var[1]
-- 拼接并返回
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"(集群中的)RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_
jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTim
e":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
- 3)功能测试
执行nginx -s reload
命令重新加载,并刷新页面:
可见,OpenResty集群已经获取到了前端传递的参数并拼接后返回。
6.5.4 查询Tomcat
OpenResty集群获取到商品ID后,本应该去Nginx本地缓存、Redis缓存中查询商品信息,但目前测试项目还未建立Nginx、Redis缓存。
因此,这里可以先根据商品ID去Tomcat服务器查询商品信息,如图:
需要注意的是,OpenResty集群部署在虚拟机,IP地址是192.168.146.128,而Tomcat是直接运行在Windows系统上的,其IP地址的前三位和虚拟机一致,最后一位改为1即可,即192.168.146.1。
6.5.4.1 发送HTTP请求的API
Nginx提供了内部API用于发送HTTP请求,其格式如下:
local resp = ngx.location.capture("/path",{
method = ngx.HTTP_GET, -- 请求方式
args = {a=1,b=2}, -- get方式传参数
body = "c=3&d=4" -- post方式传参数
})
返回的响应内容包括:
- resp.status:响应状态码
- resp.header:响应头,是一个table
- resp.body:响应体,即响应数据
以该API发送的HTTP请求会被Nginx内部的server监听并处理,因此要把这个请求发送到Tomcat,则需要在server中对这个路径做反向代理。
在Tomcat服务器中,查询商品信息的请求路径前缀是/dzdp/item
,那么OpenResty集群中的server就可以这样配置:
location /dzdp/item {
# Tomcat服务器的IP和端口
proxy_pass: http://192.168.146.1:8081/dzdp/item;
}
经过这样的配置之后,只要调用ngx.location.capture("/dzdp/item")
发起请求,就会被反向代理到Windows上的Tomcat服务器。
6.5.4.2 封装HTTP工具
OpenResty启动时,会加载/usr/local/openresty/lualib
目录下的工具文件,因此自定义的HTTP工具也要放在这个目录下。
在/usr/local/openresty/lualib
目录下,新建一个common.lua
文件,内容如下:
-- /usr/local/openresty/lualib/common.lua
-- 封装函数,发送GET请求,并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
-- 记录错误信息,返回404
ngx.log(ngx.ERR, "HTTP请求查询失败, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http
}
return _M
使用的时候,可以利用require('common')
来导入该函数库,这里的common就是函数库的文件名。
6.5.4.3 实现商品查询
修改/usr/local/openresty/nginx/lua/item.lua
文件,利用刚封装好HTTP工具实现对Tomcat的查询:
-- /usr/local/openresty/nginx/lua/item.lua
-- 1.引入自定义的工具类
local common = require("common")
local read_http = common.read_http
-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
local itemJSON = read_http("/dzdp/item/".. id, nil)
-- 4.返回商品信息
ngx.say(itemJSON)
执行nginx -s reload
重新加载,在页面发起请求ID=3的商品信息:
在请求ID=4的商品信息:
可见,Tomcat服务器确实接收到了请求。以上的配置均生效了。
6.5.4.4 使用CJSON工具类
OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。 /usr/local/openresty/lualib
目录下已经包含了该模块,可以直接使用:
因此,这里对/usr/local/openresty/nginx/lua/item.lua
文件进行进一步优化:
-- /usr/local/openresty/nginx/lua/item.lua
-- 1.引入自定义的工具类和cjson
local common = require("common")
local read_http = common.read_http
local cjson = require('cjson')
-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
local itemJSON = read_http("/dzdp/item/".. id, nil)
-- 4.根据ID发起请求查询商品库存信息
local itemStockJSON = read_http("/dzdp/item/stock/".. id, nil)
-- 5.将JSON转换为table
local item = cjson.decode(itemJSON)
local itemStock = cjson.decode(itemStockJSON)
-- 6.组合数据
item.stock = itemStock.stock
item.sold = itemStock.sold
-- 7.把item序列化为Json,并返回
ngx.say(cjson.encode(item))
执行nginx -s reload
重新加载,在页面发起请求ID=5的商品信息:
可见,跟前面比起来,返回的数据中多了库存信息,说明以上配置也生效了。
6.5.4.5 基于商品ID实现负载均衡
在以上测试中,Tomcat是单机部署的。而在实际项目中,Tomcat通常是集群部署。因此,OpeResty需要对Tomcat做负载均衡。
OpenResty默认的负载均衡策略是轮询。但由于Tomcat中的JVM进程缓存是不会共享的,所以对于同一个请求,在一部分Tomcat服务中可以命中JVM进程缓存,在一部分又无法命中,因此缓存的命中率较低。
- 1)原理分析
那要怎么解决呢?如果能让同一个商品,每次查询都访问同一个Tomcat服务,那么JVM进程缓存就一定能生效。也就是说,要根据商品ID做负载均衡,而不是轮询。
Nginx提供了基于请求路径做负载均衡的算法:根据请求路径做Hash运算,把得到的数值对Tomcat服务的数量取余,余数是几,就访问第几个服务,从而实现负载均衡。
例如:请求路路径为/dzdp/item/1
,Tomcat服务总数为2(端口分别是8080、8081),对请求路径做Hash运算并对2取余的结果为1,则访问第一台Tomcat服务器,即8080。
后续请求只要商品ID不变,请求路径就不变,那取余运算结果就不变,最终访问的Tomcat服务就不变,这就实现了根据商品ID做负载均衡的功能。
- 2)代码实现
修改/usr/local/openresty/nginx/conf/nginx.conf
文件,实现基于商品ID做负载均衡:
http {
# ...
# 定义Tomcat集群,设置基于路径做负载均衡
upstream tomcat-cluster {
hash $request_uri;
server 192.168.146.1:8080;
server 192.168.146.1:8081;
}
server {
listen 8082;
location /dzdp/item {
# Tomcat服务器的IP和端口
# proxy_pass http://192.168.146.1:8081/dzdp/item;
# 反向代理目标指向Tomcat集群
proxy_pass http://tomcat-cluster;
}
# ...
}
server {
listen 8083;
location /dzdp/item {
# Tomcat服务器的IP和端口
# proxy_pass http://192.168.146.1:8081/dzdp/item;
# 反向代理目标指向Tomcat集群
proxy_pass http://tomcat-cluster;
}
# ...
}
# ...
}
修改完成后,执行nginx -s reload
命令重新加载OpenResty。
- 3)功能测试
利用IDEA,分别启动两个Tomcat服务,端口分别是8080和8081:
在页面发起请求ID=1的商品信息,请求负载到8080端口的Tomcat:
在页面发起请求ID=2的商品信息,请求负载到8081端口的Tomcat:
再在页面发起请求ID=3的商品信息,请求负载到8080端口的Tomcat:
至此,基于商品ID实现负载均衡完成。
6.5.5 查询Redis
根据多级缓存的架构,在查询Tomcat之前,应先查询Redis缓存。
6.5.5.1 Redis缓存预热
Redis缓存会面临冷启动问题:
-
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加到缓存,则可能会给数据库带来较大压力。
-
缓存预热:在实际开发中,可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
在本测试项目中,由于数量量较少,且没有数据统计相关功能,因此可以在启动时将所有数据都放入缓存中。
缓存预热需要在项目启动时完成,可以利用InitializingBean接口来实现,因为InitializingBean接口会在对象被Spring创建并且成员变量全部注入后执行。 代码如下:
// com.star.redis.dzdp.config.RedisHandler
@Slf4j
@Component
public class RedisHandler implements InitializingBean {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IItemService itemService;
@Resource
private IItemStockService itemStockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
log.info("========> begin init Item to Redis.");
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
if(itemList != null && !itemList.isEmpty()) {
log.info("itemList.size = {}", itemList.size());
for (Item item : itemList) {
// 2.1 序列化
String itemJsonStr = MAPPER.writeValueAsString(item);
// 2.2 存入Redis
stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), itemJsonStr);
log.info("set to Redis: Key = {}, Value = {}", "item:id:" + item.getId(), itemJsonStr);
}
}
// 3.查询商品库存信息
List<ItemStock> itemStockList = itemStockService.list();
// 4.放入缓存
if(itemStockList != null && !itemStockList.isEmpty()) {
log.info("itemStockList.size = {}", itemStockList.size());
for (ItemStock itemStock : itemStockList) {
// 2.1 序列化
String itemStockJsonStr = MAPPER.writeValueAsString(itemStock);
// 2.2 存入Redis
stringRedisTemplate.opsForValue().set("item:stock:" + itemStock.getItemId(), itemStockJsonStr);
log.info("set to Redis: Key = {}, Value = {}", "item:stock:" + itemStock.getItemId(), itemStockJsonStr);
}
}
log.info("========> end init Item to Redis.");
}
}
启动项目,查看日志:
可见,在项目启动时会执行InitializingBean口的afterPropertiesSet
方法,以加载商品信息到Redis中。
6.5.5.2 封装Redis工具
OpenResty提供了操作Redis的模块,直接引入即可使用。为了使用方便,可以将对Redis的操作封装到之前编写的common.lua
工具库中。修改/usr/local/openresty/lualib/common.lua
文件:
- 1)引入Redis模块,并初始化Redis对象
-- 导入Redis模块,并进行初始化
local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
- 2)封装函数,用于释放Redis连接
-- 封装函数,释放Redis连接
local function close_redis(red)
-- 连接池空闲时间,单位是好秒
local pool_max_idle_time = 1000
-- 连接池大小
local pool_size = 100
local ok, err = red:set_keepalive(pool_max_idel_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "[放入redis连接池失败" .. err .. "]")
end
end
- 3)封装函数,根据Key查询Redis数据
-- 封装函数,根据key查询Redis数据
local function read_redis(ip, port, key)
-- 获取连接
local ok, err = red:connect(ip, port)
ok, err = red:auth("123321")
if not ok then
ngx.log(ngx.ERR, "[连接redis失败" .. err .. "]")
return nil
end
-- 查询Redis
local resp, err = red:get(key)
if not resp then
ngx.log(ngx.ERR, "[查询redis失败" .. err "]")
end
-- 得到数据为空的处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "[查询redis数据为空" .. key .. "]")
end
close_redis(red)
return resp
end
- 4)导出函数(read_http函数是之前封装的)
-- 将方法导出
local _M = {
read_http = read_http,
read_redis = read_redis
}
return _M
6.5.5.3 实现Redis查询
接下来修改/usr/local/openresty/nginx/lua/item.lua
文件,实现Redis查询。其查询逻辑是:根据商品ID查询Redis,如果查询失败则继续查询Tomcat,并将查询结果返回。
修改后的/usr/local/openresty/nginx/lua/item.lua
文件内容如下:
-- 1.导入组件
local common = require("common")
local read_http = common.read_http
local read_redis = common.read_redis
local cjson = require('cjson')
-- 封装函数,查询Redis数据
function read_data(key, path, params)
-- 查询Redis
local val = read_redis("192.168.146.128", 6379, key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "[Redis查询失败,尝试查询HTTP," .. key .. "]")
-- Redis查询失败,去查询HTTP
val = read_http(path, params)
else
ngx.log(ngx.ERR, "[Redis查询成功," .. key .. "]")
end
-- 返回数据
return val
end
-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
-- local itemJSON = read_http("/dzdp/item/".. id, nil)
local itemJSON = read_data("item:id:" .. id, "/dzdp/item/" .. id, nil)
ngx.log(ngx.ERR, "[查询商品信息结果: " .. itemJSON .. "]")
-- 4.根据ID发起请求查询商品库存信息
-- local itemStockJSON = read_http("/dzdp/item/stock/".. id, nil)
local itemStockJSON = read_data("item:stock:" .. id, "/dzdp/item/stock/" .. id, nil)
ngx.log(ngx.ERR, "[查询库存信息结果: " .. itemStockJSON .. "]")
-- 5.将JSON转换为table
if string.len(itemJSON) > 0 and string.len(itemStockJSON) > 0 then
ngx.log(ngx.ERR, "查询成功...")
local item = cjson.decode(itemJSON)
local itemStock = cjson.decode(itemStockJSON)
-- 6.组合数据
item.stock = itemStock.stock
item.sold = itemStock.sold
-- 7.把item序列化为Json,并返回
ngx.say(cjson.encode(item))
else
ngx.log(ngx.ERR, "查询结果为空...")
ngx.say({})
end
6.5.5.4 功能测试
所有代码编写完成后,下面进行测试。由于Redis中已经保存了ID为1~5的商品信息,所以在调用在页面发起请求ID=2的商品信息时,会直接从Redis缓存中返回:
在页面发起请求ID=8的商品信息时,会查询Redis缓存失败,然后去Tomcat中查询:
至此,查询Redis功能实现。
…
本节完,下一节继续进行多级缓存的实现。
本节所涉及的代码和资源可从git仓库下载:https://gitee.com/weidag/redis_learning.git
更多内容请查阅分类专栏:Redis从入门到精通
感兴趣的读者还可以查阅我的另外几个专栏:
- SpringBoot源码解读与原理分析(已完结)
- MyBatis3源码深度解析(已完结)
- 再探Java为面试赋能(持续更新中…)