缓存和数据库一致性

在引入 Redis 缓存后,如何保证缓存和数据库(如 MySQL)的数据一致,是后端开发中最经典也最棘手的问题之一。

因为缓存和数据库是两套完全独立的存储系统,无法实现原生的跨系统分布式事务。在高并发下,只要读写请求的时序稍微错位,就可能导致两边数据不一致。

工业界通常不追求绝对的“强一致性”(因为会严重牺牲性能),而是追求“最终一致性”,即允许数据在极短的时间窗口内不一致,但能保证经过有限时间后,两边数据最终是对齐的。

下面为你拆解最主流的解决方案以及进阶的兜底策略:

🥇 行业标准方案:旁路缓存模式(Cache Aside Pattern)

这是目前 90% 以上互联网业务首选的方案,核心逻辑非常简单,分为读和写两个流程:

  • 读流程(固定不变)
    1. 先查 Redis 缓存,如果命中,直接返回数据。
    2. 如果缓存未命中,再去查数据库。
    3. 将数据库查到的最新数据写入 Redis,并返回结果。
  • 写流程(核心争议点)
    先更新数据库,再删除缓存

❓ 为什么是“删除缓存”而不是“更新缓存”?

  1. 懒加载,避免写放大:如果频繁修改数据但没人读取,更新缓存就是浪费计算资源。删除缓存后,只有下次真正有读请求时,才会触发数据库查询并回填缓存。
  2. 维护成本低:如果缓存是复杂的聚合对象(例如包含用户信息、订单数、等级等),每次更新都需要重新计算所有字段,而直接删除则简单高效。

❓ 为什么是“先更新数据库”而不是“先删缓存”?
如果是“先删缓存,再更新数据库”,在数据库还没更新完的间隙,如果有一个读请求进来,发现缓存没了,就会去数据库查到旧数据并回填到缓存。紧接着数据库更新完成,此时缓存里存的依然是旧数据,导致了长时间的脏数据。

⚠️ 标准方案的潜在风险与“延迟双删”

“先更新数据库,再删除缓存”虽然已经是最佳实践,但在极端并发或数据库主从架构下,依然存在极短的“不一致窗口”:
假设读请求在缓存失效的瞬间去查数据库,如果此时数据库主从同步有延迟,读请求可能从从库读到了旧值并回填到缓存。

为了缓解这个问题,衍生出了“延迟双删”策略:

  1. 先更新数据库。
  2. 删除一次缓存。
  3. 线程休眠一小段时间(例如 500ms)。
  4. 再次删除缓存。

第二次删除的目的,就是为了把“并发窗口期内,被其他读请求回填的旧缓存”再次清掉。

🛡️ 生产环境的终极兜底:Binlog 异步监听

“延迟双删”虽然有效,但休眠时间很难把控,且如果第二次删除因为网络抖动失败了怎么办?因此,在大型分布式系统中,通常会引入更可靠的兜底机制:监听数据库的 Binlog(变更日志)

核心流程:

  1. 业务代码只负责更新数据库,不再直接操作缓存(或者只负责第一次删除)。
  2. 引入中间件(如阿里的 CanalDebezium)伪装成 MySQL 的从库,实时订阅数据库的 Binlog。
  3. 当中间件监听到数据变更事件后,自动发送消息去删除或更新 Redis 中的对应缓存。

这种方案彻底解耦了业务代码和缓存逻辑,即使业务层删除缓存失败,Binlog 监听层也能保证最终把脏缓存清理掉。

📊 方案对比总结

为了让你更直观地选择,可以参考下表:

方案 一致性强度 实现复杂度 性能成本 适用场景
先更库后删缓存 最终一致性(绝大多数场景够用) 默认首选,90%的业务场景
延迟双删 最终一致性(比标准方案更可靠) 对一致性要求稍高,且能接受短暂延迟的场景
Binlog 异步监听 最终一致性(极高可靠性) 高(需维护中间件) 系统复杂、微服务众多、追求业务纯净的大型系统
分布式锁强一致 强一致性 极高(串行化) 金融核心交易、库存扣减等绝不容错场景

💡 避坑补充:防止缓存雪崩

在设置缓存过期时间时,千万不要给大批量的 Key 设置完全相同的过期时间(比如都设为凌晨 2 点过期)。一旦到达时间点,缓存集体失效,海量请求会瞬间击穿到数据库,导致数据库 CPU 飙升至 100% 甚至宕机(这就是缓存雪崩)。
最佳实践:在基础过期时间上,增加一个随机偏移量(例如基础 1 小时 + 随机 0~10 分钟),让缓存的失效时间均匀分散开。

posted @ 2026-05-06 23:02  圣祖帝皇  阅读(36)  评论(0)    收藏  举报