“怎么保证缓存和数据库的数据一致性?”

很多同学上来就背八股文:“延时双删、加分布式锁…”

这道题考的不是死记硬背,而是你有没有处理过真实的线上事故

因为在生产环境中,细节才是魔鬼。今天我们就从“青铜”到“王者”,拆解 4 种方案,并揭秘 3 个连老鸟都容易踩的致命误区

❌ 方案1:先删缓存,再更新 DB(青铜)

这是很多新手的直觉反应:“先把旧缓存清了,再改数据库,下次读的时候不就读到新的了吗?”

错!这是个巨大的坑。 只要你的系统有一点并发量,这个方案就是个 Bug 制造机。

💥 翻车现场还原

看看下面这个时序图,你就知道为什么不能用了:

结局:MySQL 是新的,Redis 是旧的。而且因为 Redis 里有数据,后续请求都不会去查库,这个脏数据会一直存在,直到过期。


✅ 方案2:先更新 DB,再删缓存(黄金 - Cache Aside)

这是业界最常用的 Cache Aside Pattern

逻辑

  1. 先更新 MySQL。

  2. 更新成功后,删除 Redis 缓存。

为什么这个比方案 1 好?
虽然理论上它也存在并发问题,但在实际生产中,这种情况发生的概率极低
因为“数据库写操作”通常比“缓存读写”慢得多。要触发 Bug,需要读请求在“写请求更新完 DB 但还没删缓存”的极短微秒级时间窗口内完成整个操作,这需要极其巧合的时间差。

适用场景:90% 的读多写少业务。


✅ 方案3:延迟双删(钻石 - 高并发优化)

如果你无法容忍方案 2 中那“万分之一”的概率,或者你的数据库主从延迟比较大,延迟双删是性价比最高的方案。

核心逻辑
先删缓存 -> 更新 DB -> 休眠 N 毫秒 -> 再删缓存

为什么要删两次?

  • 第一次删:为了腾地儿。

  • 第二次删:为了把“在更新 DB 期间,其他读请求可能写入的脏数据”给清理掉。

💻 生产级代码实现

这里有个细节:休眠时间怎么定?

经验公式:Sleep 时间 ≈ (主从同步延迟 + 读请求平均耗时) * 1.5
一般设置为 300ms - 500ms。


🏆 方案4:Canal + MQ(王者 - 金融级一致性)

如果你做的是金融、支付业务,连“应用层删缓存失败”都不能容忍,那就必须把应用层解耦。

原理:应用层只管写 MySQL,由 Canal 伪装成 MySQL Slave 监听 Binlog,投递到 MQ,消费者负责删缓存并自动重试

🛑 深度思考:金融业务的“双标策略”

很多人问:“Canal 也有延迟,金融业务怎么能忍?”
这里有一个认知误区。真实的金融架构采用了双标策略

链路类型

场景

策略

是否查缓存

交易链路

转账、扣款

强一致性 (ACID)

🚫 绝对不查!直接悲观锁查 DB

展示链路

查余额、账单

最终一致性

✅ 查缓存

结论:Canal + MQ 保证的是“展示链路”的数据最终一定是正确的,不会出现“删缓存失败”导致的永久脏数据。


💣 避坑指南:3 个让你背 P0 事故的“隐形误区”

方案选对了就稳了吗?下面这三个坑,踩中一个就是生产事故。

🧨 误区 1:在事务(@Transactional)里面删缓存

这是 Spring 开发中最容易犯的低级错误!

❌ 错误代码

后果:Redis 删了,DB 还没提交。读请求进来读到旧值写回 Redis,随后事务提交。Redis 永远是旧值。

✅ 修正方案:利用事务同步管理器

🧨 误区 2:针对“热点 Key”直接删缓存

对于微博热搜、秒杀库存这种 Top Hot Key千万不能直接删!
删掉缓存的瞬间,几万个请求会直接击穿到 MySQL,数据库瞬间 CPU 100% 宕机。

✅ 修正
针对超热点 Key,使用双写更新(允许短暂不一致)或分布式锁(只放一个线程回写)。

🧨 误区 3:迷信延迟双删的 Sleep 时间

Sleep(500ms) 只是一个经验值。如果网络抖动,主从延迟飙升到 1 秒,你睡 500ms 也是白搭。
记住:延迟双删只能降低不一致概率,无法根除


📊 总结一张表

方案

一致性

复杂度

适用场景

先删缓存再更新DB

不推荐 (Bug多)

先更新DB再删缓存

⭐⭐⭐

90% 通用业务

延迟双删

⭐⭐⭐⭐

高并发、主从延迟大

Canal + MQ

⭐⭐⭐⭐⭐

核心资金业务 (展示层)

你在开发环境中用的是哪种方案?有没有踩过“事务内删缓存”的坑?欢迎在评论区聊聊你的“血泪史”!