本地缓存(如 Caffeine、Guava Cache)是节点本地的内存缓存,分布式场景下各节点缓存独立,易出现 "数据不一致"(如节点 A 更新了数据库,节点 B 的缓存还是旧数据),解决方案如下:
1、核心挑战
-
数据孤岛:每个节点的缓存仅存储本地访问的数据,无法感知其他节点的更新;
-
并发更新:多节点同时更新同一数据,可能导致 "部分节点缓存未更新";
-
延迟问题:即使触发缓存更新,网络延迟也可能导致短期不一致。
2、解决方案
方案 1:事件通知(消息队列)
-
核心原理:通过消息队列(如 Kafka、RabbitMQ)广播 "数据更新事件",所有节点监听事件,收到后删除 / 更新本地缓存。
-
实现步骤:
-
节点 A 更新数据库后,向消息队列发送事件:
{type: "delete", key: "user:1001"}
; -
所有节点(包括 A 自己)订阅该消息队列,收到事件后执行
cache.delete("user:1001")
; -
后续节点查询 "user:1001" 时,缓存未命中,查数据库获取最新数据并写入缓存。
-
-
优缺点:
-
优点:一致性较好(延迟低),实现灵活;
-
缺点:依赖消息队列(需保证消息不丢失、不重复);
-
-
适用场景:中高一致性要求、节点数适中的场景(如电商商品缓存)。
方案 2:发布 / 订阅模式(如 Redis Pub/Sub)
-
核心原理:基于 Redis 的发布 / 订阅功能,更新节点发布 "缓存更新消息",其他节点订阅消息并更新缓存。
-
实现步骤:
-
所有节点订阅 Redis 的 "cache_update" 频道;
-
节点 A 更新数据库后,向 "cache_update" 频道发布消息:
"delete:user:1001"
; -
其他节点收到消息后,解析出 key,执行
cache.delete("user:1001")
。
-
-
优缺点:
-
优点:轻量级(无需额外部署消息队列),实时性高;
-
缺点:Redis Pub/Sub 不保证消息持久化(节点离线则丢失消息);
-
-
适用场景:对一致性要求一般、可接受短期不一致的场景(如非核心业务缓存)。
方案 3:版本号控制
-
核心原理:为每个缓存 key 关联一个 "版本号",查询时先对比版本号,不一致则更新缓存。
-
实现步骤:
-
数据库表新增 "version" 字段(如 user 表的 version,每次更新 + 1);
-
缓存存储 "数据 + 版本号":
cache.set("user:1001", {data: ..., version: 3})
; -
节点 B 查询时,先查缓存的版本号(3),再查数据库的版本号(4);
-
若缓存版本号 < 数据库版本号,删除缓存,重新查询数据库写入缓存。
-
-
优缺点:
-
优点:不依赖外部组件,一致性可控;
-
缺点:每次查询多一次数据库版本号检查(性能略有损耗);
-
-
适用场景:对一致性要求较高、不希望依赖中间件的场景(如核心配置缓存)。
方案 4:定时全量同步
-
核心原理:后台线程定期从数据库全量加载数据,覆盖本地缓存,保证最终一致性。
-
实现步骤:
-
节点启动时,全量加载 "热门商品列表" 到本地缓存;
-
后台线程每 5 分钟执行一次全量同步:
List<Product> products = db.query("select * from product where is_hot=1")
; -
用新数据覆盖本地缓存:
cache.putAll(products.stream().collect(toMap(p->p.getId(), p->p)))
。
-
-
优缺点:
-
优点:实现简单,适合全量数据场景;
-
缺点:一致性弱(同步周期内数据不一致),全量同步性能损耗大;
-
-
适用场景:数据变化频率低、对一致性要求低的场景(如静态配置、热门商品列表)。
3、综合最佳实践
-
核心数据:事件通知(Kafka)+ 版本号控制(双重保障);
-
非核心数据:发布 / 订阅(Redis Pub/Sub);
-
静态数据:定时全量同步 + 过期时间兜底;
-
兜底策略:所有本地缓存设置合理的过期时间(如 5-10 分钟),即使一致性方案失效,过期后也会自动更新。