【Redis】多级缓存
1. 传统缓存的问题
传统的缓存策略一般是请求到达 tomcat
后,先查询redis,如果未命中则查询数据库。这种方式存在以下两个问题:
- 请求要经过
tomcat
处理,tomcat
的性能成为整个系统的瓶颈。 - redis缓存失效时,会对数据库产生冲击。
2. 多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻tomcat的压力,提升服务性能:
注:用作缓存的nginx是业务nginx,需要部署为集群,再使用专门的nginx用来做反向代理
2.1 JVM进程缓存
2.1.1 本地进程缓存
本地进程缓存:缓存在日常开发中起到了至关重要的作用,由于是存在在内存中,数据的读取速度非常快,能大量减少对数据库的访问,减少数据库的压力,我们把缓存分为两类:
-
分布式缓存,例如Redis:
- 优点:存储容量大,可靠性更好,可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高,需要在集群见共享
-
本地进程缓存,例如HashMap,GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限,可靠性较低,无法共享
- 场景:性能要求较高,缓存数据量较小
2.1.2 Caffeine
本地进程缓存:Caffeine
是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库,目前spring内部的缓存使用的就算Caffeine。
引入依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
示例:
@Test
void testBasicOps() {
//构建cache对象
Cache<String, String> cache = Caffeine.newBuilder().build();
//存数据
cache.put("gf", "刘亦菲");
//取数据
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
//取数据,如果未命中,则查询数据库
//参数1:缓存的key
//参数2:lambda表达式,表达式的参数就是缓存的key,方法体就是查询逻辑
//优先根据key查询jvm缓存,如果未命中,则执行参数2的lambda表达式
String defaultGF = cache.get("defaultGF", key -> {
//根据key去数据库查询数据
return "王祖贤";
});
System.out.println("defaultGF = " + defaultGF);
System.out.println("defaultGF = " + cache.getIfPresent("defaultGF"));
}
运行结果如下:
Caffeine
提供了三种缓存驱逐策略:
-
基于容量:设置缓存的数量上限
Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(10_000)//上限为10000个key .build();
-
基于时间:设置缓存的有效时间
// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时 .build();
-
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
在默认的情况下,当一个缓存元素过期的时候,Caffeine
不会自动立即将其清理和驱逐,而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
2.2 Nginx缓存
2.2.1 准备工作
首先需要安装 OpenResty
,它本质上也是一个nginx服务器,它具有以下特点:
- 具备Nginx的完整功能
- 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块
- 允许使用Lua自定义业务逻辑、自定义库
安装好 OpenResty
后,安装目录为 /usr/local/openresty
。
在 /usr/local/openresty/nginx/conf
目录下的nginx.conf文件添加如下模块:
# 加载lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 加载c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
对 /api/item
这个路径进行监听:
location /api/item {
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
注:lua/item.lua的lua目录和nginx同级,完整路径为/usr/local/openresty/lua,在这个文件下面就可以编写缓存脚本。
2.2.2 请求参数处理
参数格式 | 参数示例 | 参数解析代码示例 |
---|---|---|
路径占位符 | /item/1001 | |
请求头 | id:1001 | – 获取请求头,返回值是table类型 local headers = ngx.req.get_headers() |
Get请求参数 | ?id=1001 | – 获取GET请求参数,返回值是table类型 local getParams = ngx.req.get_uri_args() |
Post表单参数 | id=1001 | – 读取请求体 ngx.req.read_body() – 获取POST表单参数,返回值是table类型 local postParams = ngx.req.get_post_args() |
JSON参数 | {“id”:1001} | – 读取请求体 ngx.req.read_body() – 获取body中的json参数,返回值是string类型 local jsonBody = ngx.req.get_body_data() |
2.2.3 nginx发送http请求tomcat
需求:
-
获取请求参数中的id
-
根据id向Tomcat服务发送请求,查询商品信息
-
根据id向Tomcat服务发送请求,查询库存信息
-
组装商品信息、库存信息,序列化为JSON格式并返回
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:响应体,就是响应数据
注意:这里的path是路径,并不包括IP地址和端口,这个请求会被nginx内部的server监听并处理。但是我们希望这个请求能够被发送到tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:
location /path {
# 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
proxy_pass http://192.168.150.1:8081;
}
2.2.3.1 封装http查询函数
我们可以把http查询的请求封装为一个函数,放到OpenResty函数库中,方便以后使用。
-
在/usr/local/openresty/lualib目录下创建common.lua文件:
vi /usr/local/openresty/lualib/common.lua
-
在common.lua中封装http查询的函数:
-- 封装函数,发送http请求,并解析响应 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 not found, path: ", path , ", args: ", args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http } return _M
2.2.3.2 使用http函数查询数据
OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。它可以用来把多个对象通过序列化和反序列化组合成一个对象。
引入cjson模块:
--导入cjson库
local cjson = require('cjson')
序列化:
local obj = {
name = 'jack',
age = 21
}
local json = cjson.encode(obj)
反序列化:
local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)
综合实践:
修改之前编写的item.lua文件:
--导入common函数库
local common = require('common')
local read_http = common.read_http
--导入cjson库
local cjson = require('cjson')
--获取路径参数
local id = ngx.var[1]
--查询商品信息
local itemJSON = read_http("/item/"..id,nil)
--查询库存信息
local stockJSON = read_http("/item/stock/"..id,nil)
--JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
--组合数据
item.stock = stock.stock
item.sold = stock.sold
--把item序列化为json 返回结果
ngx.say(cjson.encode(item))
修改完item.lua脚本后,我们要将openresty中的nginx.conf配置也做相应修改。
将反向代理修改为如下配置:
# tomcat集群配置
upstream tomcat-cluster{
hash $request_uri;#一致性hash,一直访问有缓存的节点
server 192.168.150.1:8081;
server 192.168.150.1:8082;
}
# 反向代理配置,将/item路径的请求代理到tomcat集群
location /item {
proxy_pass http://tomcat-cluster;
}
2.2.4 nginx查询redis缓存
比起直接从nginx查询tomcat,先去查询redis显然是一种更好的方式。
2.2.4.1 缓存预热
编写一个类实现 InitializingBean
接口,实现其中的方法,就可以使得该方法在该类被注入到容器,完成依赖注入后就会执行该方法:
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ItemService itemService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Autowired
private IItemStockService stockService;
@Override
public void afterPropertiesSet() throws Exception {
//1.初始化缓存
//2.查询商品信息
List<Item> list = itemService.list();
for (Item item : list) {
//3.放入缓存
String json = MAPPER.writeValueAsString(item);
stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
//4.查询库存信息
List<ItemStock> stockList = stockService.list();
for (ItemStock itemStock : stockList) {
//5.放入缓存
String json = MAPPER.writeValueAsString(itemStock);
stringRedisTemplate.opsForValue().set("item:stock:id:" + itemStock.getId(), json);
}
}
}
2.2.4.2 查询redis缓存
OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:
-
引入Redis模块,并初始化Redis对象
-- 引入redis模块 local redis = require("resty.redis") -- 初始化Redis对象 local red = redis:new() -- 设置Redis超时时间 red:set_timeouts(1000, 1000, 1000)
-
封装函数,用来释放Redis连接,其实是放入连接池
-- 关闭redis连接的工具方法,其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, "放入Redis连接池失败: ", err) end end
这些操作都要添加到common.lua文件中,common.lua的完整内容如下所示:
--导入redis
local redis = require('resty.redis')
--初始化redis
local red = redis:new()
red:set_timeouts(1000,1000,1000)
-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
end
end
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port,password, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接redis失败 : ", err)
return nil
end
-- 发送密码验证命令
local res, err = red:auth(password)
if not res then
ngx.log(ngx.ERR, "redis认证失败: ", err)
return
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
end
close_redis(red)
return resp
end
-- 封装函数,发送http请求,并解析响应
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,
read_redis = read_redis
}
return _M
需求:
- 修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat
- 修改item.lua,查询商品和库存时都调用read_data这个函数
完整的item.lua内容如下所示:
--导入common函数库
local common = require('common')
local read_http = common.read_http
--导入redis库
local read_redis = common.read_redis
--导入cjson库
local cjson = require('cjson')
--封装查询函数,先查询redis,再查询http
function read_data(key,path,params)
--查询redis
local resp = read_redis("127.0.0.1",6379,"redis",key)
--判断查询结果
if not resp then
ngx.log("redis查询失败,尝试查询http,key:",key)
resp = read_http(path,params)
end
return resp
end
--获取路径参数
local id = ngx.var[1]
--查询商品信息
local itemJSON = read_data("item:id:"..id,"/item/"..id,nil)
--查询库存信息
local stockJSON = read_data("item:stock:id:"..id,"/item/stock/"..id,nil)
--JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
--组合数据
item.stock = stock.stock
item.sold = stock.sold
--把item序列化为json 返回结果
ngx.say(cjson.encode(item))
2.2.5 查询nginx本地缓存
OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。
-
开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m lua_shared_dict item_cache 150m;
-
操作共享词典:
-- 获取本地缓存对象 local item_cache = ngx.shared.item_cache -- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期 item_cache:set('key', 'value', 1000) -- 读取 local val = item_cache:get('key')
需求:
- 修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat
- 查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期
- 商品基本信息,有效期30分钟
- 库存信息,有效期1分钟
修改之前item.lua文件中的read_data函数:文章来源:https://www.toymoban.com/news/detail-419887.html
--封装查询函数,先查询redis,再查询http
function read_data(key,expire,path,params)
--查询本地缓存
local val = item_cache:get(key)
if not val then
ngx.log(ngx.ERR,"本地缓存查询失败,尝试查询redis,key:",key)
--查询redis
val = read_redis("127.0.0.1",6379,"redis",key)
--判断查询结果
if not val then
ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)
val = read_http(path,params)
end
end
--查询成功,把数据写入本地缓存(重置了缓存的时间)
item_cache:set(key,val,expire)
--返回数据
return val
end
3. 总结
文章来源地址https://www.toymoban.com/news/detail-419887.html
到了这里,关于【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!