缓存与数据库的双写一致性问题,本质上是分布式系统中两个数据源的同步问题。由于 Redis 和 MySQL 通常无法通过普通操作实现强一致性,因此实际项目中一般追求最终一致性,需要根据业务场景在性能、复杂度和一致性之间做取舍
1. 先更新缓存,再更新数据库
先修改 Redis,再修改 MySQL。
如果更新缓存成功,但数据库更新失败,就会导致缓存和数据库不一致。另外,并发请求也可能导致旧数据覆盖新数据。
缺点: 两步操作不是原子操作,存在数据不一致的风险,一般不推荐。
2. 先更新数据库,再更新缓存
先修改 MySQL,再修改 Redis。
如果数据库更新成功,但缓存更新失败,就会产生不一致。此外,多个并发请求可能按照不同的顺序更新缓存,导致旧数据覆盖新数据。
缺点: 存在并发覆盖和部分操作失败的问题,一般不推荐直接采用。
3. 先删除缓存,再更新数据库
先删除 Redis 缓存,再修改 MySQL。
假设线程 A 删除了缓存,但还没来得及更新数据库。此时线程 B 查询缓存未命中,从数据库读取旧数据并重新写入缓存。随后线程 A 更新数据库,最终导致缓存与数据库不一致。
缺点: 删除缓存和更新数据库之间存在时间窗口,一般不推荐单独使用。
4. 先更新数据库,再删除缓存
这是最常用的 Cache Aside(旁路缓存)模式。
查询流程:
- 先查询 Redis。
- 如果缓存命中,直接返回数据。
- 如果缓存未命中,查询 MySQL。
- 将查询结果写入 Redis,再返回数据。
更新流程:
- 先更新 MySQL。
- 再删除 Redis 中对应的缓存。
- 后续请求发现缓存未命中,就从 MySQL 重新读取最新数据并回填缓存。
优点: 实现简单,适用于大多数业务场景。
注意: 它不能彻底解决并发读写导致的不一致,也不能保证严格的实时一致性。如果删除缓存失败,还需要重试机制等补偿措施。
5. 延迟双删
通常指先删除缓存、更新数据库,等待一段时间后再删除一次缓存。
第二次删除的目的是清理并发请求可能重新写入的旧缓存,从而降低数据不一致的概率。
缺点: 延迟时间难以准确确定,而且不能覆盖所有并发时序,因此不能保证彻底解决一致性问题。
6. 基于 MySQL Binlog 的异步同步
通过 Canal 等工具监听 MySQL 的 Binlog,将数据变更发送到消息队列,再由消费者更新或删除 Redis 缓存。
优点: 数据库变更与缓存同步解耦,适合对异步同步和可恢复性有要求的场景。
缺点: 存在同步延迟,需要处理消息失败、重复消费、重试和故障恢复等问题,通常实现最终一致性,而不是强一致性。
二、删除缓存失败怎么办?
例如,MySQL 更新成功,但 Redis 删除失败,旧缓存就可能继续被读取。
常见的解决方式有:
- 消息队列重试: 将删除缓存的任务可靠地投递出去,失败后重试。
- Binlog 订阅: 监听数据库变更,异步执行缓存删除或更新。
- 设置 TTL: 给缓存设置合理的过期时间,降低旧数据长期存在的风险。
需要注意,TTL 只是兜底手段,不能保证缓存立即与数据库一致。
三、面试总结
Redis 与 MySQL 的一致性问题,常见方案包括先更新缓存再更新数据库、先更新数据库再更新缓存、先删缓存再更新数据库、先更新数据库再删缓存、延迟双删,以及基于 Binlog 的异步同步。
实际项目中,最常用的基础方案是先更新数据库,再删除缓存。如果删除失败,可以通过消息重试或 Binlog 订阅进行补偿,并设置合理的缓存过期时间作为兜底。
如果并发量较高,还可以根据业务需求引入互斥锁、版本校验等机制。需要严格强一致性的场景,则必须进一步设计协调机制,不能仅依赖普通的缓存删除策略。
核心记忆:普通业务采用 Cache Aside,更新时先操作数据库、再删除缓存;通过重试、异步同步和 TTL 降低不一致风险,通常追求最终一致性。