亲爱的小伙伴们:
有没有过这样的经历?你明明把数据库更新了,可用户刷出来的页面还是老数据!这就好比你明明换了女友,朋友还在社交平台@你和前任的照片。🙃
这,就是Redis 缓存与 MySQL 数据库一致性问题——缓存,可能会“背叛”你!
一、一致性种类,先分个类
在正式讲缓存一致性之前,咱们得先整明白“一致性”到底分几种:
| 一致性类型 | 定义 | 举个栗子 |
|---|---|---|
| 强一致性 | 写入成功后,所有读请求立刻能读到最新值 | 银行转账、支付 |
| 弱一致性 | 写入成功后,读请求可能能读到最新值 | 一般业务场景 |
| 最终一致性 | 写入成功后,经过一段时间,读到的一定一致 | 微博点赞、浏览量、缓存系统 |
✅ 关键认知:
- 强一致性 → 读写“实时一致”
- 最终一致性 → 短时间不一致可以接受,但最终肯定一致
二、Redis + MySQL 到底是什么一致性?
答案是:
只能做到“最终一致性” 。
为什么?
- MySQL:强一致(事务ACID保证)
- Redis:无事务、异步、非阻塞 → 快速但不保证一致性
- Redis + MySQL:分布式异构系统 → 没有全局事务 → 只能靠“补救”保证最终一致
✅ 你写库 → 删缓存 → 这两步不是原子操作 → 并发下没法100%保证实时一致。
三、为什么会不一致?
Redis 和 MySQL 本质上是“两套世界”——
- Redis:我快!我轻!我不靠谱!
- MySQL:我慢!我稳!我靠谱!
当你执行:
- 写 MySQL(更新数据)
- 删除 Redis 缓存
看似合理,但并发一来:
- 线程A:删缓存 → 正在写MySQL
- 线程B:查缓存→发现没了→查库(读到旧值)→写回Redis!
🙃 于是——你刚更新的数据,被另一个并发“复原”了,缓存回滚成了老数据!
四、缓存双删:先砍一刀,再补一刀
缓存双删的思路很简单:
- 先删缓存(防止并发读老数据)
- 写 MySQL
- 再删缓存一次(防止并发把旧数据写回缓存)
redis.delete("user:123"); // 第1刀
userMapper.update(user); // 写库
Thread.sleep(50); // 等一等
redis.delete("user:123"); // 第2刀
✅ 好处:实现简单,不需要额外中间件。
❌ 坏处:sleep时间不可控,并发量大时没法保证真的删得干净。
五、延迟双删:扔个炸弹,晚点炸
延迟双删 = 缓存双删 + 延迟异步清理
- 删除缓存
- 写 MySQL
- 发消息(MQ、延时任务)
- 延迟几百毫秒 → 消费消息 → 再删缓存
redis.delete("user:123");
userMapper.update(user);
mq.send("delete-cache", user.getId());
// 消费端
Thread.sleep(200);
redis.delete("user:123");
✅ 好处:
- 非阻塞,不卡主线程
- 延迟时间灵活调整
- 高并发更稳
❌ 坏处:
- 需要MQ支持
- 系统复杂度提高
六、其他常用一致性方案(加餐时间)
除了缓存双删和延迟双删,还有这些常见方案:
| 策略 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| Cache‑Aside(先更新 DB 再删缓存) | 写入数据库后删除缓存,下次读触发缓存重建 | 简单,广泛使用;删除后新读自动刷新缓存 (yunpengn.github.io) | 删除失败会导致长期脏缓存;短时间内缓存命中率下降 |
| 异步删除 + 重试机制 | 写 DB 后异步发送删除操作(消息队列),失败可重试 | 提高删除可靠性,侵入式较小;适合高可用场景 | 引入 MQ 和重试逻辑,复杂度上升 |
| Binlog 驱动缓存失效 | 订阅 MySQL binlog(如 Canal),解析写操作后清缓存 | 几乎不改业务逻辑,最终一致;可处理 DB-master/slave 延迟 | 需额外组件(Canal),处理顺序与延迟问题 |
| 写入缓存然后异步写 DB(Write‑Behind) | 写先写 Redis,再异步写入 DB | 写快,读性能佳;支持批量 / 顺序写入 | 可能丢数据,顺序错乱,设计复杂 |
| 写穿/写直达(Write‑Through/Write‑Behind) | 写请求通过缓存统一,缓存同步写数据库(同步或异步) | 提高一致性,操作统一 | Redis 不是持久存储;失效恢复麻烦,性能受限 |
| 读穿(Read‑Through) | 读取由缓存自动去 DB 填充 | 对应用透明,无需手动缓存逻辑 | Redis 默认不支持;需插件或中间层 |
| 逻辑删除 + 异步删除缓存 | DB 标记为删除,最终由异步流程清缓存 | 写操作快,对缓存命中影响小 | 多线程协调复杂,需额外逻辑支持 |
| 短过期时间 + 母鸡式双删 | 缓存大幅缩短 TTL,写 DB 后删除缓存 | 简单稳定,过期兜底;降低脏数据持续时间 | 缓存命中率下降,读频繁命中 DB,增加压力 |
| 分布式锁 + DB→缓存更新 | 写 DB 后获取锁更新缓存,避免并发写混乱 | 并发安全,减少缓存抖动 | 增加延迟,分布锁成本,性能下降 |
不同场景、不同权衡,合理选择。
七、总结发布版:别让缓存“背叛”你
- 缓存双删:同步+两次删除,简单有效但不精准
- 延迟双删:异步+延迟删除,性能更优雅
- 其他方案:读写穿透、版本控制、主库强读
一致性本质:
- Redis + MySQL → 只能做到最终一致性
- 真正的强一致性 → 需要事务、同步锁、或者牺牲性能
现实里:
- 支付、金融 → 强一致性
- 社交、电商、内容 → 最终一致性
缓存就像爱情——太快的不一定靠谱,慢一点的反而更牢靠。
不追求完美,但追求“最终正确”。别让你的缓存,把你坑了!😄