> 文章列表 > 【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

【Redis】多级缓存

文章目录

  • 【Redis】多级缓存
    • 1. 传统缓存的问题
    • 2. 多级缓存方案
      • 2.1 JVM进程缓存
        • 2.1.1 本地进程缓存
        • 2.1.2 Caffeine
      • 2.2 Nginx缓存
        • 2.2.1 准备工作
        • 2.2.2 请求参数处理
        • 2.2.3 nginx发送http请求tomcat
          • 2.2.3.1 封装http查询函数
          • 2.2.3.2 使用http函数查询数据
        • 2.2.4 nginx查询redis缓存
          • 2.2.4.1 缓存预热
          • 2.2.4.2 查询redis缓存
        • 2.2.5 查询nginx本地缓存

1. 传统缓存的问题

传统的缓存策略一般是请求到达 tomcat 后,先查询redis,如果未命中则查询数据库。这种方式存在以下两个问题:

  1. 请求要经过 tomcat 处理,tomcat 的性能成为整个系统的瓶颈。
  2. redis缓存失效时,会对数据库产生冲击。

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)


2. 多级缓存方案

多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻tomcat的压力,提升服务性能:

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

注:用作缓存的nginx是业务nginx,需要部署为集群,再使用专门的nginx用来做反向代理


2.1 JVM进程缓存

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

2.1.1 本地进程缓存

本地进程缓存:缓存在日常开发中起到了至关重要的作用,由于是存在在内存中,数据的读取速度非常快,能大量减少对数据库的访问,减少数据库的压力,我们把缓存分为两类:

  1. 分布式缓存,例如Redis:
    • 优点:存储容量大,可靠性更好,可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高,需要在集群见共享
  2. 本地进程缓存,例如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"));
}

运行结果如下:

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)


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服务器,它具有以下特点:

  1. 具备Nginx的完整功能
  2. 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块
  3. 允许使用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 {# 响应类型,这里返回jsondefault_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 【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)
请求头 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

需求:

  1. 获取请求参数中的id

  2. 根据id向Tomcat服务发送请求,查询商品信息

  3. 根据id向Tomcat服务发送请求,查询库存信息

  4. 组装商品信息、库存信息,序列化为JSON格式并返回

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

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函数库中,方便以后使用。

  1. 在/usr/local/openresty/lualib目录下创建common.lua文件:

    vi /usr/local/openresty/lualib/common.lua
    
  2. 在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-- 记录错误信息,返回404ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)ngx.exit(404)endreturn 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显然是一种更好的方式。

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

2.2.4.1 缓存预热

编写一个类实现 InitializingBean 接口,实现其中的方法,就可以使得该方法在该类被注入到容器,完成依赖注入后就会执行该方法:

@Component
public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate ItemService itemService;private static final ObjectMapper MAPPER = new ObjectMapper();@Autowiredprivate IItemStockService stockService;@Overridepublic 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的模块,我们只要引入该模块就能直接使用:

  1. 引入Redis模块,并初始化Redis对象

    -- 引入redis模块
    local redis = require("resty.redis")
    -- 初始化Redis对象
    local red = redis:new()
    -- 设置Redis超时时间
    red:set_timeouts(1000, 1000, 1000)
    
  2. 封装函数,用来释放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 thenngx.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 thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 发送密码验证命令local res, err = red:auth(password)if not res thenngx.log(ngx.ERR, "redis认证失败: ", err)returnend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)endclose_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-- 记录错误信息,返回404ngx.log(ngx.ERR, "http 查询失败, path: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
end-- 将方法导出
local _M = {  read_http = read_http,read_redis = read_redis
}  
return _M

需求:

  1. 修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat
  2. 修改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)--查询redislocal resp = read_redis("127.0.0.1",6379,"redis",key)--判断查询结果if not resp thenngx.log("redis查询失败,尝试查询http,key:",key)resp = read_http(path,params)endreturn 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,大小150mlua_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')
    

需求:

  1. 修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat
  2. 查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期
  3. 商品基本信息,有效期30分钟
  4. 库存信息,有效期1分钟

修改之前item.lua文件中的read_data函数:

--封装查询函数,先查询redis,再查询http
function read_data(key,expire,path,params)--查询本地缓存local val = item_cache:get(key)if not val thenngx.log(ngx.ERR,"本地缓存查询失败,尝试查询redis,key:",key)--查询redisval = read_redis("127.0.0.1",6379,"redis",key)--判断查询结果if not val thenngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)val = read_http(path,params)endend--查询成功,把数据写入本地缓存(重置了缓存的时间)item_cache:set(key,val,expire)--返回数据return val
end

货币汇率网