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

news2025/6/20 4:03:33

【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缓存失效时,会对数据库产生冲击。

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/1001image-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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/426356.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Huffman 编码

1.Huffman编码 1952年提出一种编码方法&#xff0c;该方法完全依据字符出现概率来构造异字头的平均长度最短的码字&#xff0c;有时称之为最佳编码&#xff0c;一般就叫做Huffman编码(有时也称为霍夫曼编码)。 2.Huffman树 树是一种重要的非线性数据结构&#xff0c;它是数据元…

​2023年十大目标检测模型!

“目标检测是计算机视觉中最令人兴奋和具有挑战性的问题之一&#xff0c;深度学习已经成为解决该问题的强大工具。”—Dr. Liang-Chieh Chen目标检测是计算机视觉中的基础任务&#xff0c;它涉及在图像中识别和定位目标。深度学习已经革新了目标检测&#xff0c;使得在图像和视…

【CV大模型SAM(Segment-Anything)】真是太强大了,分割一切的SAM大模型使用方法:可通过不同的提示得到想要的分割目标

目录前言安装运行环境SAM模型的使用方法导入相关库并定义显示函数导入待分割图片使用不同提示方法进行目标分割方法一&#xff1a;使用单个提示点进行目标分割方法二&#xff1a;使用多个提示点进行目标分割方法三&#xff1a;用方框指定一个目标进行分割方式四&#xff1a;将点…

文件操作和IO—javaEE

文章目录1.文件1.1文件系统的结构1.2java中的文件操作&#xff08;metadata的操作&#xff09;2.io操作2.1定义2.2io划分2.3java的io流之输入流2.4java的io流之输出流1.文件 文件包含数据本身和文件的头信息&#xff08;metadata&#xff09;&#xff0c;文件的头信息包括文件…

VSCode的C/C++编译调试环境搭建(亲测有效)

文章目录前言1.安装VSCode和mingw642.配置环境变量3.配置VSCode的运行环境3.1设置CodeRunner3.2设置C/C4.调试环境配置前言 这片博客挺早前就写好了&#xff0c;一直忘记发了&#xff0c;写这篇博客之前自己配的时候也试过很多博客&#xff0c;但无一例外&#xff0c;都各种js…

SpringBoot(4)整合数据源

SpringBoot整合数据源数据层解决方案数据源技术持久化技术数据库技术NoSQL整合Redis整合MongDB整合ES数据层解决方案 MySQL数据库与MyBatisPlus框架&#xff0c;后面又用了Druid数据源的配置&#xff0c;所以现在数据层解决方案可以说是MysqlDruidMyBatisPlus。而三个技术分别…

一文彻底了解派克Parker无铁芯/有铁芯直线电机及其应用

一、什么是直线电机&#xff1f; 直线电机是一种将电能直接转换成直线运动机械能&#xff0c;而不需要任何中间转换机构的传动装置。它可以看成是一台旋转电机按径向剖开&#xff0c;并展成平面而成。 二、直线电机的特点 直线电机类似于一台旋转电机解剖摊开来进行运转。在一…

9、DRF实战总结:过滤(filter)与排序,以及第三方库django-filter的使用(附源码)

在前面的DRF系列教程中&#xff0c;以博客为例介绍了序列化器(Serializer), 并使用基于类的视图APIView和ModelViewSet开发了针对文章资源进行增删查改的完整API接口&#xff0c;并详细对权限、认证(含jwt认证)和分页进行了总结与演示。在本篇文章中将向演示如何在Django REST …

Boost库在windows上的使用

今天要配置一个C环境&#xff0c;被Boost库困扰了一段时间&#xff0c;在这里记录一下解决的方法。 主要是打不开 libboost_iostreams-vc143-mt-gd-x64-1_82.lib这样的问题。 操作的步骤如下&#xff1a; 下载binary包&#xff1a; 链接: https://boostorg.jfrog.io/artifac…

ChatGPT有用到知识图谱吗?它自己是这样回答...

从搜索引擎到个人助手&#xff0c;我们每天都在使用问答系统。问答系统必须能够访问相关的知识并进行推理。通常&#xff0c;知识可以隐式地编码在大型语言模型&#xff08;LLMs&#xff09;中&#xff0c;例如ChatGPT、T5 和LaMDA 等大型语言模型&#xff0c;这些模型在未结构…

如何面对人生困境至暗时刻

北方春天伊始刚好想发表下另一种境遇就是当人生面临困境或者至暗怎么样走出来&#xff0c;如果有正面临这样的情况来分享下如何走出阴霾&#xff0c;拥有更多可能性的人生&#xff0c;现在回望过去一年的自己太过牵强失去自我。 对世界的应该思维&#xff1a;为什么我总看不清现…

PHP快速入门10-图像处理,附图像大小调整、旋转、获取颜色等15个常见示例

文章目录前言一、PHP的图像处理1.1 图像处理函数二、 图像处理示例2.1 创建一个空白的图像2.2 从文件创建一个新的图像2.3 从URL创建一个新的图像2.4 调整图像大小2.5 对比度和亮度调整2.6 度数旋转2.7 模糊滤镜2.8 获取图像的颜色信息2.9. 图像合并2.10 图像旋转和裁剪2.11 图…

css补充内容

1.最好给body设置min-width,防止缩小页面时出现空白 2.让图片随着网页缩小而缩小 3.html5语义化元素 4.video与audio video是行内替换元素 默认是第一帧静态画面,需要手动调整画面大小和添加播放条,是否自动播放 大多数浏览器不支持自动播放,除非设置为muted,这是为了用户的…

小巧“抠门”的FTHR-F0140开发板

小巧“抠门”的FTHR-F0140开发板 文章目录小巧“抠门”的FTHR-F0140开发板缘起硬件电路主控芯片供电系统调试器插座LED灯按键CAN接口电路软件资源资源链接缘起 工欲善其事&#xff0c;必先利其器。调试和开发MM32F0140这种小巧的芯片&#xff0c;还是需要小巧的板子去适配&…

本科也可入行的IC模拟版图,需要学习哪些知识?

IC模拟版图是一个入门非常简单&#xff0c;但同时又是一份涉及知识面非常广阔&#xff0c;资深较难的工作。 在众多IC岗位中&#xff0c;模拟版图确实属于容易入门&#xff0c;吸引来很多想要转行IC行业的朋友&#xff0c;但需要掌握的知识点和技巧并不比设计少&#xff0c;属…

Python+Selenium+Unittest 之selenium5--元素定位4-XPath定位1(基本概念)

目录 一、简介 二、节点关系 三、绝对路径与相对路径 一、简介 Xpath定位在selenium中属于常用的定位方式&#xff0c;首先来说下Xpath的一些概念&#xff0c;Xpath即为XML路径语言&#xff08;XML Path Language&#xff09;&#xff0c;它是一种用来确定XML文档中某部分位置…

Vision Transformers for Dense Prediction论文笔记

文章目录Vision Transformers for Dense Prediction&#xff0c; ICCV&#xff0c; 2021一、背景介绍二、网络结构三、实验结果1.语义分割实验2.消融实验Vision Transformers for Dense Prediction&#xff0c; ICCV&#xff0c; 2021 一、背景介绍 本篇论文主要提出一种网络…

SpringBoot 集成webSocket

pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 …

IT软件行业用契约锁实现“代理-销售-投标-项目-合作”电子签

IT软件行业产品销售渠道多、销售订单体量大&#xff0c;从产品研发、销售到项目实施&#xff0c;伴随着大量的协议、合同、单据需要与员工、客户或者经销商签署。引入电子签章&#xff0c;化解纸质合同签署带来的效率、成本、安全等问题&#xff0c;成为软件行业产品代理、销售…

Vector - CAPL - Panel面板_01

前面有过简单的介绍panel面板的功能&#xff0c;不过终究感觉有点简陋&#xff0c;最近也在搞PyQT5&#xff0c;发现如果对于这块了解不多的情况下&#xff0c;想要做一些东西的话&#xff0c;简直无从下手&#xff0c;因此专门翻阅了之前的文章&#xff0c;查看了下确实缺少了…