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

【Redis】多级缓存

1. 传统缓存的问题

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

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

image-20230413203944261


2. 多级缓存方案

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

image-20230413204807451

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


2.1 JVM进程缓存

image-20230413205008322

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"));
}

运行结果如下:

image-20230413210838934


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 {
     # 响应类型,这里返回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 image-20230417180357977image-20230417180415994
请求头 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格式并返回

image-20230417180831361

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
            -- 记录错误信息,返回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显然是一种更好的方式。

image-20230417220915053

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的模块,我们只要引入该模块就能直接使用:

  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 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

需求:

  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)
    --查询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')
    

需求:

  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 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. 总结

image-20230423231532508

猜你喜欢

转载自blog.csdn.net/Decade_Faiz/article/details/130190343