【Redis|高级篇2】多级缓存|JVM进程缓存、Lua语法、多级缓存实现(OpenResty)、缓存同步(Canal)

接下来继续学习Redis的高级篇,内容包含:分布式缓存多级缓存Redis最佳实践 相关内容

本篇主要是介绍Redis的多级缓存,为了减轻了 Redis 的网络 I/O 压力和 Tomcat(业务应用)的 CPU/处理压力,具体有:JVM进程缓存Lua语法多级缓存实现(OpenResty)以及缓存同步(Canal)

欢迎大家一起学习,共同进步🥰

文章目录

2.多级缓存

传统缓存的问题

传统的缓存策略一般是请求到Tomcat后,先查Redis,如果未命中则查询数据库,存在的问题:

  • 请求要通过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
  • Redis缓存失效时,会对数据库产生冲击

多级缓存

多级缓存的核心思想是构建一个纵深防御体系,在请求处理的每个环节都设置缓存层,让数据离用户更近、读取更快,从而极大地提升系统性能。

  1. 浏览器/客户端缓存
    • 位置:用户的浏览器或App本地。
    • 作用:作为第一道防线,直接缓存静态资源(如图片、CSS、JS文件)。对于未过期的资源,请求甚至不会发送到服务器,极大地减少了网络传输和服务器压力。
  2. CDN缓存 (内容分发网络)
    • 位置:分布在全球各地的边缘服务器。
    • 作用:将网站的静态内容分发到离用户地理位置最近的节点。当用户请求资源时,直接从最近的CDN节点获取,显著降低访问延迟。
  3. Nginx本地缓存
    • 位置:网关或反向代理服务器(如集成Lua的OpenResty)。
    • 作用:这是多级缓存架构的关键升级。请求在进入应用服务器之前,会先经过Nginx。如果Nginx本地缓存命中,可直接返回数据,请求完全不会进入后端的Tomcat等应用集群,极大地减轻了应用服务器的压力。
  4. 分布式缓存 (如Redis)
    • 位置:独立的缓存服务器集群。
    • 作用:这是传统缓存架构的主力。它容量大,可以被所有应用服务器共享,用于存储热点业务数据、用户会话等。
  5. 应用进程缓存 (如JVM缓存)
    • 位置:应用服务器自身的内存中(如使用Caffeine、Guava Cache)。
    • 作用:速度最快(无网络开销),作为最后一道屏障。当分布式缓存出现故障或网络抖动时,它可以防止请求直接击穿到数据库,起到"救命"的作用。

2.1JVM进程缓存

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

  • 分布式缓存 eg:Redis
    • 优点:存储容量更大,可靠性更高,可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大,可靠性要求较高,需要在集群间共享
  • 进程本地缓存 eg:HashMap
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限,可靠性较低,无法共享
    • 场景:性能要求较高,缓存数据量较小
2.1.1初识Caffeine

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前spring内部的缓存使用的就是Caffeine。

引入依赖:

xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version> <!-- 建议使用最新稳定版本 -->
</dependency>

示例

  1. 创建caffeine实例
  2. 存数据put
  3. 取数据getIfPresentget
java 复制代码
@Test
void testBasicOps() {
    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder().build();

    // 存数据
    cache.put("gf", "迪丽热巴");

    // 取数据,不存在则返回null
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);

    // 取数据,不存在则去数据库查询
    String defaultGF = cache.get("defaultGF", key -> {
        // 这里可以去数据库根据 key查询value
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

Caffeine的三种缓存驱逐策略

Caffeine 的缓存驱逐机制是其高性能的核心保障。简单来说,它通过自动管理内存,防止缓存无限增长导致内存溢出(OOM),同时确保留在缓存里的都是最有价值的数据。

  • 基于容量:设置缓存的数量上限

    java 复制代码
    Cache<String, String> shopCache = Caffeine.newBuilder()
                    .maximumSize(1)//缓存大小上限为1
                    .build();
  • 基于时间:设置缓存的有效时间

    java 复制代码
    Cache<String, String> shopCache = Caffeine.newBuilder()
                    .expireAfterWrite(Duration.ofSeconds(10))//有效期为十秒
                    .build(); 
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用

默认情况下,当一个缓存元素过期时,caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐

原因

  1. 避免精准定时带来的巨大开销

    caffeine不关心数据"精确"在几毫秒过期。它只关心:"当你下次来访问(读/写)这个数据时,我发现它已经过期了,那我就顺手把它删掉。"

  2. 保证读写操作的无锁和极速

  3. 节省CPU资源

2.1.2实现进程缓存

1.初始化缓存,用Bean注释将它交给Spring容器管理,设置参数

java 复制代码
@Configuration
public class CaffeineConfig {

    //商品缓存
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
            	.initalCapacity(100)
                .maximumSize(10000) 
                .build();
    }
    
    //库存缓存
    @Bean
    public Cache<Long, ItemStock> stockCache() {
        return Caffeine.newBuilder()
            	.initalCapacity(100)
                .maximumSize(10000) 
                .build();
    }
}

2.具体业务实现

注入缓存

java 复制代码
@Autowired
private Cache<Long,Item> itemCache;
@Autowired
private Cache<Long,ItemStock> stockCache;

Spirng通过泛型类型区分Bean

java 复制代码
@GetMapping("/{id}")
public Item findById(@PathVariable("id") Long id) {
    return itemCache.get(id, key -> itemService.query()
            .ne("status", 3).eq("id", key)
            .one()
    );
}

@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id) {
    return stockCache.get(id, key -> stockService.getById(key));
}

2.2Lua语法

2.2.1初识Lua

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供林国外的拓展和定制功能

快速入门

  1. 在Linux虚拟机的任意目录下,新建一个hello.lua文件

    bash 复制代码
    touch hello.lua
  2. 添加lua脚本

    lua 复制代码
    print("hello world")
  3. 运行

    bash 复制代码
    lua hello.lua

进入lua控制台:lua

2.2.2变量和循环

数据类型

测量变量类型:type(...)

变量

Lua声明变量时不需要指定变量类型

lua 复制代码
-- 声明字符串
local str = 'hello'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true
-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map = {name='Jack', age=21}
特性 全局变量 local (局部变量)
作用域 全局可见,所有文件、函数都能访问 仅在声明它的"块"内有效(如函数、循环、文件内部)
生命周期 程序运行期间一直存在,直到被手动销毁 出了作用域(块结束)即被释放,由垃圾回收处理
性能 较慢(需要访问全局环境表 _G 极快(直接访问寄存器或栈)
安全性 低(容易被其他代码意外覆盖或修改) 高(外部无法访问,避免命名冲突)

字符串拼接用:..

访问table:

lua 复制代码
-- 访问数组,从角标1开始
print(arr[1])
-- 访问table
print(map['name'])
print(map.name)

数组、table都可以利用for循环来遍历

  • 遍历数组:
lua 复制代码
-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
    print(index, value)
end
  • 遍历table:
lua 复制代码
-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
    print(key, value)
end
2.2.3条件控制和函数

函数

定义函数的语法:

lua 复制代码
function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

例如,定义一个函数,用来打印数组:

lua 复制代码
function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

条件控制

类似java的条件控制

lua 复制代码
if(...)
then
	-- true时执行
else
	-- false时执行
end

2.3多级缓存

2.3.1OpenResty初识与安装

OpenResty是一个基于Nginx的高性能web平台,用于方便地搭建能够处理高并发、扩展性极高的动态web应用、web服务和动态网关

简单理解:OpenResty基于Nginx再加上一些插件

特点

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的Lua库,第三方模块
  • 允许使用Lua自定义业务逻辑、自定义库

openresty/bin/下有openresty文件,是一个软链接,链向Nginx的可执行文件:

安装

1)安装开发库

首先要安装OpenResty的依赖开发库,执行命令:

sh 复制代码
yum install -y pcre-devel openssl-devel gcc --skip-broken

2)安装OpenResty仓库

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update 命令)。运行下面的命令就可以添加我们的仓库:

复制代码
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示说命令不存在,则运行:

复制代码
yum install -y yum-utils 

然后再重复上面的命令

3)安装OpenResty

然后就可以像下面这样安装软件包,比如 openresty

bash 复制代码
yum install -y openresty

4)安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:

bash 复制代码
yum install -y openresty-opm

默认情况下,OpenResty安装的目录是:/usr/local/openresty

5)配置nginx的环境变量

打开配置文件:

sh 复制代码
vi /etc/profile

在最下面加入两行:

sh 复制代码
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME:后面是OpenResty安装目录下的nginx的目录

然后让配置生效:

复制代码
source /etc/profile
2.3.2OpenResty快速入门

具体步骤

  1. 修改nginx.conf文件

    1. 在nginx.conf的http下面,添加对OpenResty的Lua模块的加载:

      nginx 复制代码
      # 加载lua 模块
      lua_package_path "/usr/local/openresty/lualib/?.lua;;";
      # 加载c模块
      lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

      这两行配置就是配置"搜索路径" 。虽然 OpenResty 默认通常已经配置好了这些路径,但在某些自定义环境或为了保险起见,显式地告诉 Nginx "去哪里加载 Lua 代码库" ,防止程序因为找不到依赖文件而崩溃

    2. 在nginx.conf的server下面,添加对/api/item这个路径的监听:

      nginx 复制代码
      location /api/item {
          # 响应类型,这里返回json
          default_type application/json;
          # 响应数据由 lua/item.lua这个文件来决定
          content_by_lua_file lua/item.lua;
      }
  2. 编写item.lua文件

    1. 在nginx目录创建文件夹lua

      bash 复制代码
      mkdir lua
    2. 在lua文件夹下创建文件:item.lua

      bash 复制代码
      touch lua/item.lua
      lua 复制代码
      ngx.say('{"id":1001,"name":"SALSAS"}')
    3. 重新加载配置

      bash 复制代码
      nginx -s reload 
2.3.3请求参数处理
2.3.4查询Tomcat

Nginx内部提供了可以发送http请求的API:

lua 复制代码
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来对这个路径做反向代理:

nginx 复制代码
location /path {
    # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
    proxy_pass http://192.168.150.1:8081;
}
封装请求函数

我们可以把http查询的请求封装为一个函数,放到OpenRe函数库中,方便后期使用。

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

bash 复制代码
vi /usr/local/openresty/lualib/common.lua

配置了查找路径,方便业务代码随时按需引用

后续执行 require "common"时,OpenResty就会加载这个文件

2.在common.lua中封装http查询的函数

lua 复制代码
-- 封装函数,发送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
  1. 为什么要编写、封装、导出这三步?
    • 因为加了local,表示当前函数只在这个lua文件有效,外部无法使用
  2. 那删去local行不行?
    • 可以,但很危险
    • 如果去掉 local,这个函数就变成了全局函数
    • 坏处:
      • 污染全局环境 :所有的脚本都能看到这个函数,变量名很容易冲突(比如你在 A 文件定义了 read_http,B 文件也定义了一个同名的,就会打架)
      • 性能略差:Lua 查找全局变量比查找局部变量(local)要慢一点点
发送请求
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))

OpenResty提供了一个cjson的模块用于处理JSON的序列化和反序列化


·Tomcat的负载均衡
  1. 配置Tomcat集群
  2. 反向代理配置,将请求代理到Tomcat集群

如果不用 hash request_uri默认策略就是轮询

OpenResty(准确说是底层的 Nginx)会严格地按照顺序,一个接一个地把请求发给后端的 Tomcat 节点

缺点:每台机器都要重复干活,缓存无法共享

使用了hash $request_uri后:相同的 URL 永远找同一台 Tomcat

目标服务器索引 = hash(请求特征) % 服务器总数

hash算法就是把任意长度的字符串(或数据),转换成一个固定长度的整数

eg:"abc" -----> 97 *31 *31 + 98 *31 + 99

2.3.5Redis缓存预热

冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

我们数据量较少,可以在启动时将所有数据都放入缓存中。

eg:把商品信息缓存进Redis

java 复制代码
@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private IItemService itemService;

    @Autowired 
    private RedisTemplate redisTemplate; 

    private static final ObjectMapper MAPPER = new ObjectMapper();

    // 在属性设置完成后执行
    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化缓存
        // 1.查询商品信息(从数据库全量拉取)
        List<Item> itemList = itemService.list();
        
        // 2.放入缓存(遍历写入Redis)
        for (Item item : itemList) {
            // 2.1.item序列化为JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }
    }
}

implements InitializingBean

  • 这是一个 Spring 的接口。它的意思是:"当这个类被 Spring 创建好,并且所有属性都注入完成后,立即执行 afterPropertiesSet 方法。

ObjectMapper

  • Jackson 库里专门负责JSON 转换的工具。这里它的核心任务就是把 Java 对象转成 JSON 字符串,
2.3.6查询Redis缓存

引入模块+初始化 -> 建立连接 -> 执行能力 -> 归还连接,放入连接池

这段代码演示了如何在 OpenResty 环境下通过 Lua 脚本操作 Redis,主要涵盖了模块的引入初始化以及连接池的封装。

OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:

  1. 引入Redis模块,并初始化Redis对象:(在common.lua文件)

    lua 复制代码
    -- 引入redis模块
    local redis = require("resty.redis")
    -- 初始化Redis对象
    local red = redis:new()
    -- 设置Redis超时时间
    red:set_timeouts(1000, 1000, 1000)
  2. 封装函数,用来释放Redis连接,其实是放入连接池:

    lua 复制代码
    -- 关闭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
  3. 封装函数,从Redis读数据并返回:

    lua 复制代码
    -- 查询redis的方法 ip和port是redis地址,key是查询的key
    local function read_redis(ip, port, key)
        -- 获取一个连接
        local ok, err = red:connect(ip, port)
        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, ", key = " , key)
        end
        --得到的数据为空处理
        if resp == ngx.null then
            resp = nil
            ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
        end
        close_redis(red)
        return resp
    end

    同样将方法导出

2.3.7Nginx本地缓存

开启共享字典 -> 获取本地缓存对象-> 存储set->读取get

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。

  • 开启共享字典,在nginx.conf的http下添加配置:

    nginx 复制代码
    # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
    lua_shared_dict item_cache 150m;
  • 操作共享字典:

    lua 复制代码
    -- 获取本地缓存对象
    local item_cache = ngx.shared.item_cache
    -- 存储,指定key、value、过期时间,单位s,默认为0代表永不过期
    item_cache:set('key', 'value', 1000)
    -- 读取
    local val = item_cache:get('key')

2.4缓存同步

2.4.1数据同步策略

缓存数据同步的常见方式有三种

  • 设置有效期 :给缓存设置有效期,到期后自动删除。再次查询时更新。
    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务
  • 同步双写 :在修改数据库的同时,直接修改缓存。
    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高
    • 场景:对一致性、时效性要求较高的业务
  • 异步通知 :修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据。
    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步
2.4.2安装Canal

初识Canal

译为水道/管道,是阿里巴巴旗下的一款开源项目,基于java开发。基于数据库增量日志解析,提供增量数据订阅和消费

1.开启MySQL主从

  1. 开启binlog

    修改文件:

    sh 复制代码
    vi /tmp/mysql/conf/my.cnf

    添加内容:

    ini 复制代码
    log-bin=/var/lib/mysql/mysql-bin
    binlog-do-db=heima

    配置解读:

    • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin
    • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库
  2. 设置用户权限

    添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。

    mysql 复制代码
    create user canal@'%' IDENTIFIED by 'canal';
    GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
    FLUSH PRIVILEGES;

    重启mysql容器即可

    复制代码
    docker restart mysql

2.安装Canal

  1. 创建网络

    创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

    sh 复制代码
    docker network create heima

    让mysql加入这个网络:

    sh 复制代码
    docker network connect heima mysql
  2. 安装Canal

    上传到虚拟机,然后通过命令导入:

    复制代码
    docker load -i canal.tar

    然后运行命令创建Canal容器:

    sh 复制代码
    docker run -p 11111:11111 --name canal \
    -e canal.destinations=heima \
    -e canal.instance.master.address=mysql:3306  \
    -e canal.instance.dbUsername=canal  \
    -e canal.instance.dbPassword=canal  \
    -e canal.instance.connectionCharset=UTF-8 \
    -e canal.instance.tsdb.enable=true \
    -e canal.instance.gtidon=false  \
    -e canal.instance.filter.regex=heima\\..* \
    --network heima \
    -d canal/canal-server:v1.1.5
2.4.3监听canal实现缓存同步

Canal客户端

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。地址:https://github.com/NormanGyllenhaal/canal-client

引入依赖:

xml 复制代码
<!--canal-->
<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

编写配置:

yaml 复制代码
canal:
  destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致
  server: 192.168.150.101:11111 # canal地址

策略一:只删除缓存

这是目前互联网大厂最主流的策略(如 Redis 官方推荐的 Cache-Aside 模式)。

核心逻辑

  • 当数据库数据变更时,只删除 Redis 中对应的 Key。
  • 下一次有请求来查询时,发现缓存没有(Miss),再去查数据库,并把结果重新写入缓存。

优点

  • 简单:只需要知道 Key 就能删,不需要关心 Value 长什么样。
  • 节省资源:如果数据改了但没人读,就不会浪费资源去更新缓存。
  • 数据绝对准确:因为是查询时从数据库重新加载的,保证了缓存和数据库完全一致。

缺点

  • 第一次查询变慢:删除后的第一次请求需要穿透到数据库,延迟较高。
  • 数据库压力大:如果瞬间有大量请求发现缓存没了,会同时冲击数据库(缓存击穿)。

策略二:直接更新缓存

这是图中代码展示的策略,通常用于对性能要求极高的场景。

核心逻辑

  • 当数据库数据变更时,利用 Canal 拿到最新的数据,直接把 Redis 中的旧值覆盖为新值

优点

  • 读性能极致:用户永远能直接读到缓存,不存在"第一次查询慢"的问题。
  • 保护数据库:彻底避免了缓存击穿的风险,数据库压力最小。

缺点

  • 代码复杂 :需要维护数据组装逻辑(如图中需要 Item 对象)。
  • 资源浪费:如果数据更新了但没人读,这次更新就是白做的。
  • 数据可能不一致:如果更新缓存失败了(比如 Redis 抖动),缓存里就会一直存着脏数据,且没有自动恢复机制(除非设置过期时间)
相关推荐
Lyyaoo.2 小时前
【JAVA基础面经】CAS 与 ABA
java·开发语言
AC赳赳老秦2 小时前
OpenClaw对接百度指数:关键词热度分析,精准定位博客创作方向
java·python·算法·百度·dubbo·deepseek·openclaw
Ava的硅谷新视界2 小时前
SQLite WAL 模式踩坑笔记:高并发读写下的几个细节
开发语言·后端·编程
小雅痞2 小时前
[Java][Leetcode middle] 274. H 指数
java·算法·leetcode
晚晚不晚2 小时前
分页查询后端实现
java
talen_hx2962 小时前
emqx的Keep alive
java·笔记·学习
huanmieyaoseng10032 小时前
Mybatis常见面试题
java·开发语言·mybatis
無限進步D7 小时前
Java 运行原理
java·开发语言·入门
難釋懷7 小时前
安装Canal
java