在分布式系统中,Redis 通常被用作缓存层来减轻数据库压力、提升读取性能。但一旦引入缓存,就不可避免地面临一个经典难题:当数据同时存在于数据库和 Redis 中时,如何保证两者的数据一致性? 这也是面试中高频出现的考点,本文将系统梳理各种方案及其优缺点。
为什么会出现双写不一致
先看一个典型的更新流程:
- 更新数据库中的值
- 删除 Redis 中的缓存
看起来没问题,但在并发场景下可能出问题。假设有两个线程 A 和 B:
- 线程 A 更新数据库为值 1
- 线程 B 更新数据库为值 2
- 线程 B 删除缓存
- 线程 A 删除缓存
由于网络延迟或调度顺序,最终缓存被删除后,下一次读取会把数据库中的值 2 加载进缓存,这看起来没问题。但如果是下面这种顺序:
- 线程 A 查询缓存,未命中
- 线程 A 查询数据库,得到旧值 1
- 线程 B 更新数据库为值 2
- 线程 B 删除缓存
- 线程 A 将旧值 1 写入缓存
此时缓存中是旧值 1,数据库中是值 2,出现了长期不一致,直到缓存过期才会恢复。
常见解决方案
1. 先更新数据库,再删除缓存(Cache Aside)
这是最常用的方案,也叫旁路缓存模式。读请求先读缓存,未命中则读数据库并回写缓存;写请求先更新数据库,再删除缓存。
优点:实现简单,大多数场景下能保证最终一致。
缺点:极端并发下仍可能不一致(如上文所述)。但这种情况概率极低,因为要求读操作在写操作之前开始,且写操作在读完数据库后、写缓存前完成删除,时间窗口很小。
2. 延迟双删
在更新数据库前后各删除一次缓存,中间加一段延迟:
删除缓存
更新数据库
延迟 N 毫秒
再次删除缓存
延迟时间一般设置为读业务逻辑耗时加上几百毫秒,目的是把可能在第一次删除后、更新数据库前读到的旧数据再次清掉。
优点:能覆盖大部分并发不一致场景。
缺点:延迟时间难以精确设定;第二次删除失败则仍然不一致。
3. 先删除缓存,再更新数据库
这种顺序在并发下更容易出问题:
- 线程 A 删除缓存
- 线程 B 读缓存未命中,读数据库旧值,写入缓存
- 线程 A 更新数据库为新值
结果缓存中是旧值。因此一般不推荐这种顺序,除非配合延迟双删。
4. 使用消息队列保证最终一致
更新数据库后,将“删除缓存”的操作发送到消息队列,由消费者异步执行。如果删除失败,可以重试,直到成功。
优点:解耦、可重试、可靠性高。
缺点:引入 MQ 增加系统复杂度;存在短暂不一致窗口。
5. 订阅数据库 Binlog 异步删除
通过 Canal 等工具订阅 MySQL 的 Binlog,解析出数据变更后,发送消息删除对应缓存。
优点:业务代码无侵入,可靠性高,是很多大厂的实践方案。
缺点:架构复杂,需要维护 Canal、MQ 等组件。
6. 读写锁 / 分布式锁
在更新时加锁,保证同一数据的读写串行化。例如使用 Redisson 的读写锁:
- 读操作加读锁
- 写操作加写锁
优点:强一致性好。
缺点:性能下降明显,锁竞争激烈时成为瓶颈,一般只用于对一致性要求极高的场景。
如何选择
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 先更新库再删缓存 | 最终一致 | 高 | 低 | 绝大多数场景 |
| 延迟双删 | 最终一致 | 高 | 中 | 并发写较多 |
| MQ 异步删除 | 最终一致 | 高 | 中高 | 对可靠性要求高 |
| Binlog 订阅 | 最终一致 | 高 | 高 | 大型系统 |
| 分布式锁 | 强一致 | 低 | 中 | 金融级场景 |
面试回答要点
- 先指出问题本质:双写不一致源于并发下读写的交错执行,以及缓存删除失败。
- 给出主流方案:先更新数据库再删除缓存是基础,延迟双删是增强。
- 强调最终一致:在分布式系统中,强一致代价极高,通常追求最终一致。
- 补充兜底措施:设置缓存过期时间作为最后保障;删除失败时通过 MQ 重试。
- 结合场景:如果面试官追问,可以提到 Binlog 订阅方案,体现对生产实践的了解。
总结
Redis 与数据库的双写一致性没有银弹。Cache Aside + 延迟双删 + 缓存过期是性价比最高的组合,能覆盖绝大多数业务场景。对于一致性要求极高的场景,可以引入 MQ 或 Binlog 订阅来保证删除操作的可靠性。理解每种方案的取舍,比死记某一种“标准答案”更重要。
未经允许不得转载:任鹏个人博客 » Redis 缓存与数据库双写一致性问题怎么解决

