基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

本地缓存(如Guava)与Redis结合的多级缓存场景中,如何保证两级缓存之间的一致性?

知识点图片

在“本地缓存(如 Guava/Caffeine) + 分布式缓存(如 Redis)”的多级缓存架构中,保证一致性的核心难点在于:当数据发生更新时,如何通知所有应用节点清空其内存中的本地缓存。 因为 Redis 是共享的,而本地缓存是分布在各个应用服务器进程内的。

通常,保证这两级缓存与数据库之间一致性的方案有以下几种,按业务对一致性的要求从低到高排列:


1. 基于 TTL(过期时间)的被动失效(最终一致性)

这是最简单、成本最低的方案。

  • 做法:给本地缓存设置一个相对较短的过期时间(例如 5秒、10秒,或 1 分钟)。读取时先查本地,没有再查 Redis,再没有查 DB,然后依次写入 Redis 和本地缓存。更新数据时,只更新 DB 并删除 Redis。
  • 一致性表现:在本地缓存过期之前的这段时间,应用会读到脏数据。
  • 适用场景:对数据实时性要求不高、允许短时间不一致的场景(如首页推荐、热榜数据、字典配置等)。

2. 基于 Redis Pub/Sub(发布订阅)的主动失效

这是最常用的轻量级解决方案。利用 Redis 自带的发布订阅功能实现节点间的通信。

  • 写入/更新流程
    1. 节点 A 更新数据库(DB)。
    2. 节点 A 删除 Redis 中的对应缓存。
    3. 节点 A 向 Redis 的特定 Channel 发布一条消息:“Key=X 的数据已更新”。
    4. 节点 A 删除自身的本地缓存。
  • 订阅流程
    1. 所有应用节点(包括 A、B、C)在启动时订阅该 Channel。
    2. 节点 B 和 C 收到消息后,执行 guavaCache.invalidate("X") 清除自身的本地缓存。
  • 优点:实现简单,不依赖额外的中间件(复用 Redis)。
  • 缺点:Redis Pub/Sub 是“发后即忘”的(Fire-and-forget),如果某个节点当时网络抖动或重启,会漏掉消息,导致该节点本地缓存一直是脏数据(直到 TTL 过期)。
  • 优化:必须配合本地缓存的 TTL 一起使用作为兜底。

3. 基于 MQ(消息队列)的主动失效

如果对一致性要求较高,不能容忍 Pub/Sub 的消息丢失,可以引入 RabbitMQ、RocketMQ 或 Kafka。

  • 做法:与 Redis Pub/Sub 类似,只是把发布消息的媒介换成了可靠的 MQ。采用广播模式(Broadcast)消费,确保每个应用节点都能收到失效消息。
  • 优点:消息可靠投递,支持重试,节点宕机恢复后可以追溯消息,一致性更高。
  • 缺点:引入了重量级中间件,增加了系统复杂度和延迟。

4. 基于 Canal + Binlog 的异步解耦失效

为了不让缓存失效逻辑侵入业务代码,可以通过监听数据库的变更日志来实现。

  • 做法
    1. 业务代码只负责更新 MySQL。
    2. Canal 伪装成 MySQL 从节点,监听 Binlog 变更。
    3. Canal 解析 Binlog 后,将变更事件发送到 MQ。
    4. 独立的工作进程或所有应用节点消费 MQ 消息:
      • 删除 Redis 缓存。
      • 广播清除各节点的本地缓存。
  • 优点:代码完全解耦,业务层无需关心缓存一致性逻辑;基于数据库事务,非常可靠。
  • 缺点:架构较重,存在一定的异步延迟(通常在毫秒级,极端情况下可能达到秒级)。

业界成熟的开源框架推荐

在实际开发中,不建议自己从头造轮子,很多优秀的开源框架已经把多级缓存和一致性问题封装好了:

  1. J2Cache (OSChina 开源)
    • 两级缓存框架:L1 是 Ehcache/Caffeine,L2 是 Redis。
    • 内置了一致性保证机制:通过 Redis Pub/Sub 或 JGroups 广播 L1 缓存失效消息。
  2. JetCache (Alibaba 开源)
    • 支持多级缓存,提供 @Cached 等便捷注解。
    • 底层支持本地缓存(Caffeine)和远程缓存(Redis)自动同步。
  3. HotKey (京东开源)
    • 专门针对突发热点数据的框架。客户端探测到热点后自动推送到本地缓存,并在服务端数据变更时主动推送到各个客户端,保证高度一致。

关键细节与最佳实践(避坑指南)

  1. 采用“删除缓存”而非“更新缓存”
    收到广播消息时,一定要执行 Delete/Invalidate 操作,而不是 Put 操作。因为在并发场景下,Put 更新缓存极易产生并发覆盖问题,导致数据不一致。
  2. “本地缓存 TTL 兜底”绝对不能少
    无论你用 Pub/Sub 还是 MQ,都必须给 Guava/Caffeine 设置过期时间(例如 expireAfterWrite)。因为网络分区、MQ 延迟、甚至 GC 停顿都可能导致失效消息丢失或延迟,TTL 是保证最终一致性的最后一道防线。
  3. 避免缓存击穿(缓存风暴)
    当本地缓存和 Redis 均被清空时,大量并发请求可能穿透到 DB。需要使用 Guava/Caffeine 的异步刷新(refreshAfterWrite)机制,或者在查询 DB 前加分布式锁,确保只有一个线程去查库重建 Redis。
  4. 注意多节点的时钟一致性
    如果业务对时间极其敏感,确保各应用节点服务器的 NTP 时钟同步。

总结建议
对于绝大部分互联网业务,“Redis Pub/Sub 广播失效 + 本地缓存短 TTL 兜底” 是性价比最高、最常见的架构选择。如果有成熟的中间件团队,可以采用 “Canal + MQ 广播” 方案来实现极致的解耦与可靠性。

00:00
00:00