1. 常见多级缓存结构
- L1:本地缓存(如Java 中的 Caffeine):进程内缓存,速度最快,容量最小
- L2:分布式缓存(如 Redis、Memcached):集群化缓存,供多节点,容量中等。
- L3:数据库(如 MySQL):底层数据源,数据最终一致性的基准。
2. 核心一致性难点
- 更新顺序问题: 数据更新时,若先更新缓存再更新数据库,可能因为数据库更新失败而产生脏数据;若先更新数据库,可能因为缓存更新失败导致不一致。
- 缓存穿透/穿透: 缓存未命中时大量请求直击数据库,或热 Key 失效瞬间引发数据库压力骤增。
- 分布式环境下的并发问题: 多节点同时读写时,可能出现一个节点更新了缓存,另一个节点仍然读取旧缓存的情况。
3. 常见方案
方案一、Cache-Aside(缓存旁路模式)
以数据库为基准,缓存仅作为“旁路”辅助,不主动参与数据更新,适用于读多写少的场景。
- 读操作
- 先查 L1 本地缓存,命中则返回
- 未命中则查询 L2 分布式缓存,命中则返回并同步到 L1
- 均未命中则查询数据库,返回数据并更新 L2 和 L1。
- 写操作
- 先更新数据库
- 再删除 L2 和 L1 缓存(非更新)
优势: 避免缓存更新时的并发冲突问题(如两个写操作同时更新缓存导致数据不一致)
注意事项: 删除缓存可能失败,需配合”重试机制”(如消息队列异步重试)或“设置缓存过期时间“兜底。
方案二、Write-Through(写透模式)
写操作先更新缓存,再由缓存同步更新数据库,确保缓存与数据库同时修改。
- 流程:
- 写操作直接更新 L1 缓存
- L1 缓存更新后,立即同步更新 L2 缓存
- L2 缓存更新后,再同步更新数据库
- 所有层级更新成功后,再返回更新成功。
优势: 缓存与数据库强一致性,适合对一致性要求极高的场景。
缺点: 写性能差(多了更新缓存的步骤),且任意一个环节失败会导致整个写操作失败。
方案三、Write-Behind(写回模式)
写操作先更新缓存,数据库异步更新(如批量延迟写入),牺牲一部分一致性换取高性能。
- 流程
- 写操作直接更新 L1 和 L2 缓存,标记为“脏数据”
- 缓存系统定期(或达到阈值)异步将“脏数据”批量写入数据库
- 数据库写入成功后,清除”脏数据“标记。
优势: 写性能极高(减少数据库 IO 次数),适合高并发写的场景(如日志收集)
风险管理: 若缓存宕机,会导致未及时同步到数据库的“脏数据”丢失,需要持久化缓存(如 Redis 的 RDB/AOF)降低风险。
方案四、基于消息队列的异步一致性
通过消息队列解耦数据库与缓存的更新,确保更新操作不会丢失,适合分布式系统。
- 流程
- 写操作先更新数据库,成功后发送“数据更新消息”到消息队列
- 消息队列消费者监听消息,依次删除/更新 L2 和 L1 缓存
- 若缓存更新失败,消息队列重试(如 RabbitMQ 的死信队列机制)。
优势: 解决“数据库更新成功但缓存更新失败“的问题,保证最终一致性。
注意: 需处理“消息重复消费”(如给消息加唯一 ID 去重)。
4. 特殊场景补充策略
4.1 缓存与数据库并发更新冲突
当多个线程同时更新同一数据时,可能出现“旧数据覆盖新数据”的问题。
- 解决方案
- 给缓存增加版本号:每次更新版本号 + 1,更新缓存时检查版本号是否大于当前缓存版本,避免旧数据覆盖
- 分布式锁:更新缓存前获取锁(如 Redis 的 SETNX),确保同一时间只有一个线程更新。
4.2 热点 Key 一致性
热点 Key(如秒杀商品库存)被高频访问,若缓存失效可能引发数据库雪崩。
解决方案
- 互斥锁: 缓存失效时,只允许一个线程查询数据库并更新缓存,其他线程等待
- 热点 Key 永不过期: 不设置过期时间,通过消息队列异步更新缓存(如库存变化时主动推送更新)
- 本地缓存兜底: L1 本地缓存保留热点 Key 的“兜底值”,即使 L2 缓存失效,也能返回较新数据。
4.3 跨层级缓存同步延迟
L1 本地缓存为进程内缓存,多节点部署时,某节点更新数据后,其他节点的 L1 缓存可能仍为旧值。
解决方案
- 缓存更新通知: 使用 发布 - 订阅 模式(如 Redis Pub/Sub),某节点更新缓存后,向其他节点发送“缓存失效通知“,触发其他节点删除本地 L1 缓存
- 缩短 L1 缓存过期时间: 让 L1 缓存的过期时间远短于 L2(如 L1 10 秒,L2 1 小时),通过快速过期减少不一致窗口。
5. 一致性与性能的平衡原则
- 不追求“强一致性”: 多级缓存的本质是”性能优化工具“,强一致性会牺牲性能,多数场景下保证“最终一致性”即可(如允许毫秒级/秒级的不一致)
- 按业务场景选方案
- 金融交易:优先使用 Write - Through + 分布式锁,保证强一致性
- 电商商品详情:用 Cache-Aside + 消息队列,允许短时间不一致;
- 日志 / 统计数据:用 Write-Behind,优先保证写入性能。
- 兜底机制: 所有方案都需要配合“缓存过期时间”,即使更新失败,过期后也能从数据库加载新数据,避免脏数据永久留存。
总结
多级缓存一致性的核心是“明确业务对一致性的容忍度”,通过更新顺序控制+异步补偿+兜底策略实现平衡。