← 返回题目列表

Redis 缓存和数据库如何保证一致性?

高频 困难 第 22 / 36 题 更新于 2026/07/27
Redis缓存一致性延迟双删Cache Aside

简化版

缓存和数据库很难做到绝对强一致,常见目标是最终一致。最常用模式是 Cache Aside:读时先查缓存,缓存没有再查数据库并回填;写时先更新数据库,再删除缓存。删除缓存通常比更新缓存更稳,因为能避免并发下写入旧值。

详细版

常见 Cache Aside 流程:

读:

  1. 查 Redis;
  2. 命中直接返回;
  3. 未命中查数据库;
  4. 写入缓存并返回。

写:

  1. 更新数据库;
  2. 删除缓存;
  3. 下一次读缓存未命中,再从数据库加载新值。

为什么常用“删缓存”而不是“更新缓存”:

  • 缓存结构可能和数据库结构不一致,更新逻辑复杂;
  • 并发写时更新缓存容易覆盖新值;
  • 有些数据写多读少,更新缓存浪费;
  • 删除后由读请求按最新数据库状态重建,更简单。

高并发场景还可以配合延迟双删、消息队列重试、binlog 订阅、版本号和短 TTL。

完整版教学

一、先接受一个现实:缓存很难强一致

Redis 和数据库是两个系统,一次业务写入要同时改变两边状态。只要不是一个真正的分布式事务,就可能出现一边成功、一边失败,或者并发读写穿插导致短暂不一致。

因此大多数业务追求的是最终一致:短时间可能不一致,但通过删除、过期、重试、补偿让缓存最终回到正确状态。

例如数据库更新成功后,应用进程在删除 Redis 前崩溃,缓存仍然保留旧值;或者缓存刚失效,读线程查到旧库值并回填,写线程随后提交新值。这些都不是某个命令写错,而是两个系统之间没有天然原子提交。面试里要先说明目标:普通缓存追求最终一致,余额、库存、支付状态这类关键事实以数据库为准。

两个系统一致性的难点:
数据库写成功 + 缓存删失败
数据库写慢 + 并发读回填旧值
缓存更新成功 + 数据库事务回滚

二、为什么不推荐先删缓存再更新数据库

流程如果是:

  1. 删除缓存;
  2. 更新数据库。

并发下可能出问题:

  1. 线程 A 删除缓存;
  2. 线程 B 读缓存未命中,查到旧数据库值;
  3. 线程 B 把旧值写回缓存;
  4. 线程 A 更新数据库为新值。

结果缓存里长期保存旧值,直到 TTL 到期。

这个问题的核心是“读线程在写事务提交前回填旧值”。如果缓存 TTL 是 30 分钟,那这 30 分钟内用户都可能读到旧数据。即使加延迟双删,也是在缩短旧值停留窗口,不是让两个系统变成强一致事务。

写入顺序主要风险
先删缓存,再更新数据库并发读可能把旧库值回填缓存
先更新数据库,再删缓存删除失败会留下旧缓存
直接更新缓存,再更新数据库数据库失败时缓存可能超前
先更新数据库,再更新缓存并发写可能旧更新覆盖新更新

三、为什么常用先更新数据库再删缓存

更常见流程是:

  1. 更新数据库;
  2. 删除缓存。

这样下一次读缓存未命中,会从数据库读到新值并回填。它也不是完美的:如果数据库更新成功但删除缓存失败,缓存仍可能是旧值。

所以生产要有补偿手段,比如删除失败重试、消息队列异步删除、订阅 binlog 删除缓存、设置合理 TTL。

先更新数据库再删缓存的好处是让数据库成为事实来源。删除缓存后,下一次读会重新从数据库加载当前值。即使删除失败,也可以通过重试、TTL 或 binlog 订阅最终把旧缓存清掉。相比“更新缓存”,删除缓存不需要在写链路里重建复杂的缓存结构,也减少并发覆盖风险。

记忆钩子:写缓存一致性时,优先把数据库当事实来源,把缓存当可丢弃的加速层;写后删缓存,是让下一次读自己重建正确值。

四、延迟双删解决什么问题

延迟双删通常是:

  1. 先删除缓存;
  2. 更新数据库;
  3. 延迟一小段时间再删除缓存。

它试图处理并发读把旧值回填到缓存的问题。延迟时间要根据业务读写耗时估算,过短没效果,过长会增加不一致窗口。

不过延迟双删不是银弹。相比之下,先更新数据库再删缓存配合可靠重试,通常更简单清晰。

延迟时间要根据读写耗时估算。比如一次数据库写事务通常 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 订阅。关键业务状态仍以数据库和业务约束为准,缓存只做加速层。