Redis 缓存和数据库如何保证一致性?
简化版
缓存和数据库很难做到绝对强一致,常见目标是最终一致。最常用模式是 Cache Aside:读时先查缓存,缓存没有再查数据库并回填;写时先更新数据库,再删除缓存。删除缓存通常比更新缓存更稳,因为能避免并发下写入旧值。
详细版
常见 Cache Aside 流程:
读:
- 查 Redis;
- 命中直接返回;
- 未命中查数据库;
- 写入缓存并返回。
写:
- 更新数据库;
- 删除缓存;
- 下一次读缓存未命中,再从数据库加载新值。
为什么常用“删缓存”而不是“更新缓存”:
- 缓存结构可能和数据库结构不一致,更新逻辑复杂;
- 并发写时更新缓存容易覆盖新值;
- 有些数据写多读少,更新缓存浪费;
- 删除后由读请求按最新数据库状态重建,更简单。
高并发场景还可以配合延迟双删、消息队列重试、binlog 订阅、版本号和短 TTL。
完整版教学
一、先接受一个现实:缓存很难强一致
Redis 和数据库是两个系统,一次业务写入要同时改变两边状态。只要不是一个真正的分布式事务,就可能出现一边成功、一边失败,或者并发读写穿插导致短暂不一致。
因此大多数业务追求的是最终一致:短时间可能不一致,但通过删除、过期、重试、补偿让缓存最终回到正确状态。
例如数据库更新成功后,应用进程在删除 Redis 前崩溃,缓存仍然保留旧值;或者缓存刚失效,读线程查到旧库值并回填,写线程随后提交新值。这些都不是某个命令写错,而是两个系统之间没有天然原子提交。面试里要先说明目标:普通缓存追求最终一致,余额、库存、支付状态这类关键事实以数据库为准。
两个系统一致性的难点:
数据库写成功 + 缓存删失败
数据库写慢 + 并发读回填旧值
缓存更新成功 + 数据库事务回滚
二、为什么不推荐先删缓存再更新数据库
流程如果是:
- 删除缓存;
- 更新数据库。
并发下可能出问题:
- 线程 A 删除缓存;
- 线程 B 读缓存未命中,查到旧数据库值;
- 线程 B 把旧值写回缓存;
- 线程 A 更新数据库为新值。
结果缓存里长期保存旧值,直到 TTL 到期。
这个问题的核心是“读线程在写事务提交前回填旧值”。如果缓存 TTL 是 30 分钟,那这 30 分钟内用户都可能读到旧数据。即使加延迟双删,也是在缩短旧值停留窗口,不是让两个系统变成强一致事务。
| 写入顺序 | 主要风险 |
|---|---|
| 先删缓存,再更新数据库 | 并发读可能把旧库值回填缓存 |
| 先更新数据库,再删缓存 | 删除失败会留下旧缓存 |
| 直接更新缓存,再更新数据库 | 数据库失败时缓存可能超前 |
| 先更新数据库,再更新缓存 | 并发写可能旧更新覆盖新更新 |
三、为什么常用先更新数据库再删缓存
更常见流程是:
- 更新数据库;
- 删除缓存。
这样下一次读缓存未命中,会从数据库读到新值并回填。它也不是完美的:如果数据库更新成功但删除缓存失败,缓存仍可能是旧值。
所以生产要有补偿手段,比如删除失败重试、消息队列异步删除、订阅 binlog 删除缓存、设置合理 TTL。
先更新数据库再删缓存的好处是让数据库成为事实来源。删除缓存后,下一次读会重新从数据库加载当前值。即使删除失败,也可以通过重试、TTL 或 binlog 订阅最终把旧缓存清掉。相比“更新缓存”,删除缓存不需要在写链路里重建复杂的缓存结构,也减少并发覆盖风险。
记忆钩子:写缓存一致性时,优先把数据库当事实来源,把缓存当可丢弃的加速层;写后删缓存,是让下一次读自己重建正确值。
四、延迟双删解决什么问题
延迟双删通常是:
- 先删除缓存;
- 更新数据库;
- 延迟一小段时间再删除缓存。
它试图处理并发读把旧值回填到缓存的问题。延迟时间要根据业务读写耗时估算,过短没效果,过长会增加不一致窗口。
不过延迟双删不是银弹。相比之下,先更新数据库再删缓存配合可靠重试,通常更简单清晰。
延迟时间要根据读写耗时估算。比如一次数据库写事务通常 50ms,读库回填缓存通常 20ms,那么第二次删除延迟可以设置在数百毫秒量级,用来覆盖并发读回填旧值的窗口。如果延迟设置 5ms,可能读线程还没回填;如果设置 10 秒,又会拉长实现复杂度和资源占用。实际项目中要用监控和压测校准,而不是随便写一个固定值。
五、什么时候需要更强方案
如果业务对一致性要求高,比如库存、余额、支付状态,不能只靠缓存。应该:
- 数据库作为最终事实来源;
- 关键写使用数据库事务和条件更新;
- 缓存只做加速;
- 读关键状态时必要时查库;
- 使用版本号防止旧值覆盖新值;
- 通过 binlog 或消息队列做可靠失效通知。
缓存不能承担核心一致性约束。
比如库存扣减,不能因为 Redis 缓存里显示还有库存就直接放行订单,最终仍应由数据库条件更新、Redis Lua 原子扣减后落库、或队列串行化来保证不超卖。缓存一致性方案解决“读到的数据尽快正确”,不是替代业务约束。缓存可以短暂错,账务和库存不能靠缓存猜。
六、失败补偿和观测很关键
只写出 Cache Aside 流程还不够,生产还要回答“删除失败怎么办”。常见做法是删除缓存失败后写入消息队列重试,或者订阅数据库 binlog 变更异步删除缓存;同时给缓存设置 TTL,作为兜底恢复。还要监控缓存删除失败、消息堆积、缓存命中率和数据库回源压力。
写路径:
更新 DB 成功
-> 删除 Redis 成功:结束
-> 删除 Redis 失败:记录重试消息
-> 重试仍失败:等待 TTL 兜底 + 告警
七、常见误区与追问
- 误区:缓存和数据库可以轻松做到绝对强一致。 Redis 和数据库是两个系统,没有分布式事务时通常只能做最终一致和补偿。
- 误区:写数据时更新缓存比删除缓存更准确。 并发写下旧值可能覆盖新值,缓存结构复杂时更新逻辑也容易出错。
- 误区:先删缓存再更新数据库最安全。 并发读可能在数据库更新前查到旧值并回填缓存,导致旧缓存停留到 TTL。
- 误区:延迟双删能解决所有一致性问题。 它只缓解特定并发回填旧值窗口,仍要配合重试、TTL 和业务约束。
- 追问:数据库更新成功但缓存删除失败怎么办? 需要可靠重试、消息队列、binlog 订阅或 TTL 兜底,并对失败链路告警。
- 追问:库存余额这类场景能只靠缓存一致性吗? 不能,关键一致性要靠数据库事务、条件更新、唯一约束、Lua 原子逻辑和幂等兜底。
八、加强记忆
缓存一致性通常追求最终一致,最常用模式是 Cache Aside:读缓存,未命中查库并回填;写先更新数据库,再删除缓存,让下一次读重建。先删缓存容易被并发读回填旧值,直接更新缓存容易被并发覆盖;删除缓存更简单,但必须配合 TTL、失败重试、消息队列或 binlog 订阅。关键业务状态仍以数据库和业务约束为准,缓存只做加速层。