← 返回题目列表

缓存和数据库的一致性怎么保证?为什么用删除缓存而不是更新缓存?

高频 困难 第 14 / 26 题 更新于 2026/10/03
缓存一致性Cache Aside双删
学习 AI 实战项目

简化版

缓存和数据库不一致的根源是一次业务写要跨两个独立系统,无法天然原子。主流 Cache Aside 在数据库事务提交成功后删除缓存,读 miss 再从权威库回填。删除是幂等的,也避免写路径重算复杂缓存,但它仍有删除失败和“旧读晚回填”窗口,必须用可靠重试、TTL、版本条件或 CDC 收敛。

详细版

Cache Aside 的基本流程:

操作流程
读先读缓存,未命中查数据库,再回填缓存
写先更新数据库,再删除缓存,下次读重新加载

为什么不是先删缓存再更新数据库?因为删缓存后、更新数据库前,如果有读请求进来,会读到数据库旧值并回填缓存,之后数据库更新完成,缓存却长期保存旧值。

删除通常比直接更新缓存更容易做对:并发写若分别计算缓存,完成顺序可能与数据库提交顺序不同;聚合缓存还可能依赖多张表,写时重算既昂贵又容易漏依赖。删除后按需加载减少了这些问题。不过,旧读请求仍可能在删除之后晚回填,所以不能把“删除”说成没有并发顺序问题。

进阶方案包括延迟双删、订阅 binlog 删除缓存、缓存过期时间兜底、重要数据读主库或版本校验。

完整版教学

一、一致性问题的根源

数据库和缓存是两个独立系统。一次写操作如果既要改数据库又要改缓存,就必然是两个步骤。只要是两个步骤,就存在第一步成功、第二步失败,或者两步之间插入并发读写的可能。

所以 Cache Aside 的目标通常是限制陈旧窗口,并保证异常后最终收敛,而不是承诺线性一致。余额扣减、权限撤销等不能接受旧值的决策,应读权威存储或使用专门的一致性协议,不能只靠旁路缓存。

回答一致性问题时,要分别说明正常顺序、删除失败、并发旧值回填和恢复上限;只说“先更新数据库再删缓存”还没有闭环。

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

先删除缓存再更新数据库有明显漏洞:线程 A 删除缓存后还没更新数据库,线程 B 读取发现缓存空,于是查数据库旧值并写回缓存。随后线程 A 更新数据库为新值。结果数据库是新值,缓存是旧值,直到 TTL 到期前都会不一致。

先更新数据库再删除缓存也不是完美,但正常路径更清楚:数据库事务 COMMIT 成功后才发起失效;若事务回滚,不应删除一个并未发生的版本。删除成功后,下一次读通常会取到新值并回填。

若删除发生在事务提交之前,另一个读请求可能在提交完成前回填旧值;若提交失败,提前删除还会制造无意义的 miss。实现时应把“数据库更新成功”定义为事务已提交,而不是 SQL 已执行但事务尚未结束。

BEGIN -> UPDATE DB -> COMMIT 成功 -> 删除缓存
                         └─ 删除失败 -> 写入可靠重试/由 CDC 补偿
COMMIT 失败 --------------------------> 不发布本次失效

三、为什么删除比更新更稳

更新缓存的问题在并发场景下很隐蔽。两个写请求 A、B 同时修改同一数据,数据库最终以 B 为准。但缓存更新可能因为网络、计算耗时不同,出现 B 先写缓存、A 后写缓存,最终缓存变成 A 的旧值。

两个写请求无论先后删除,最终都只是让 Key 不存在,因此删除本身没有“旧缓存值覆盖新缓存值”的问题,而且重复删除安全。但整个读写链路仍有另一种竞态:读 A 在数据库提交前读到 v10,写 B 提交 v11 并删缓存,随后 A 才把 v10 写回。TTL 会让它最终过期,却不能阻止这段时间的脏读。

更稳的回填会携带数据库版本。若缓存仍保留 v11,Lua/CAS 可拒绝 v10 覆盖;但删除后缓存为空,仅比较当前 value 仍会放行 v10,因此还要在独立版本键/tombstone 中保留最低可接受版本,或使用版本化 Key。若缓存里只有裸 JSON、没有版本下限,就只能接受并量化这一窗口。

四、延迟双删解决什么

先更库再删缓存仍有小概率漏洞:一个读请求先查数据库旧值,写请求随后更新数据库并删除缓存,最后这个读请求把旧值回填缓存。延迟双删是在更新数据库后立即删一次缓存,再延迟几百毫秒删第二次,清理这类并发读回填的旧值。

延迟时间要覆盖正常读库与回填耗时,并加适当余量,不是随手固定 500ms,也不是越长越好。慢查询若耗时超过延迟,仍可能在第二次删除后回填旧值;进程崩溃还可能让第二次删除根本没执行。因此延迟双删只是概率优化,第二删应进入可靠任务系统,并配合版本条件或 CDC,而不能被表述为一致性保证。

五、binlog 订阅为什么更可靠

业务进程删除 Redis 失败时,不能只打印日志。可在同一数据库事务写 outbox,提交后由任务持续重试;也可订阅数据库提交后的 binlog,由消费端幂等删除缓存。两种方式都要保存消费位置、处理重放、监控积压,并保证日志保留时间覆盖最长故障。

CDC 适合跨服务统一捕获已提交变更,但链路更长,也会出现延迟、乱序、重复和日志过期。缓存值带业务版本可拒绝旧事件或旧查询回填;TTL 则是事件链路长期失败后的时间兜底。CDC 的部署细节属于独立问题,本题重点仍是 Cache Aside 的提交边界与竞态。

六、面试追问与工程边界

“先更库再删缓存”不能保证强一致。假设缓存 TTL 为 5 分钟,数据库提交后删除连续失败,若没有可靠补偿,旧值可能持续接近 5 分钟;若 Key 没有 TTL,陈旧时间甚至没有上限。因此必须先定义业务允许的最大陈旧时间,再反推重试告警、TTL 和是否允许读缓存。

对特别关键的数据,应该少依赖缓存读结果做最终决策。例如下单扣库存时,商品详情缓存可以旧一点,但真正扣减必须回到数据库或强一致库存服务校验。缓存适合加速读,不适合替代权威判断。

还要注意读库位置。若回填从异步从库读取,数据库主库已提交 v11、缓存已删除,但从库仍是 v10,下一次 miss 会再次把旧值写入缓存。要求读己之写或高风险数据时,应读主库、等待副本达到最低 LSN/GTID,或携带业务版本拒绝倒退。

七、先用业务 SLO 选择方案

并非所有缓存都值得引入 CDC 和版本 tombstone。先问旧值会造成什么损失、最多允许旧多久,以及权威库能承受多少回源。商品文案可依靠“提交后删除 + 可靠重试 + 5 分钟 TTL”;权限撤销可能要求失效事件在秒级送达,敏感操作还必须读权威源。

数据类型可接受旧值建议策略
商品文案分钟级Cache Aside、TTL、删除重试
展示价格秒级主动失效、版本、短 TTL
权限撤销极短或不可接受事件失效,敏感操作读权威源
余额/扣库存不可用旧值决策数据库或一致性服务完成校验

监控不能只统计 Redis 删除命令成功率。还要记录数据库提交到缓存失效的延迟、重试积压、缓存 value 与数据库版本差、旧回填拒绝数和 TTL 自愈次数。通过抽样双读可以估计陈旧率,但必须控制对数据库的额外压力。

验收应主动制造 Redis 删除超时、CDC 停消费、从库延迟和旧读晚回填,观察系统是否在承诺时间内收敛。没有故障注入,只验证正常请求先后顺序,无法证明补偿链路有效。

八、失败矩阵、常见误区与追问

故障或竞态可能结果收敛手段
DB 事务回滚数据未变不发送失效事件
DB 提交成功、删除失败旧缓存继续命中outbox/CDC 重试、TTL
旧读在删除后晚回填v10 覆盖空缓存最低版本 tombstone、版本化 Key、二次删除
CDC 重复或乱序重复删除或旧事件到达幂等处理、版本比较
回填读取落后从库再次写入旧值读主、最低位点、版本约束
  • 误区:先更新缓存再更新数据库最稳。 两边更新无法原子,缓存可能写入数据库未提交或失败的数据。
  • 误区:删除缓存没有任何并发问题。 删除动作幂等,但旧读仍可能在删除后晚回填。
  • 误区:数据库 SQL 执行完即可删除缓存。 应在事务提交成功后失效,回滚不能被当作有效变更。
  • 误区:强一致缓存很容易。 缓存和数据库跨系统无法天然原子,多数场景做最终一致。
  • 追问:为什么常用删除而不是更新缓存? 删除让下一次读取从数据库加载,避免并发写入旧值。
  • 追问:删除失败怎样保证一定重试? 使用事务 outbox 或可恢复的 CDC 位点,不依赖进程内异步线程。
  • 追问:延迟双删能保证一致吗? 不能,它只覆盖部分慢读窗口,时间取值错误或任务丢失仍会失败。
  • 追问:如何阻止旧读晚回填? 回填携带业务版本,并用独立版本 tombstone 或版本化 Key 拒绝低版本;只对空缓存做 CAS 不够。
  • 追问:TTL 有什么作用? 它限定补偿永久失败时的自愈上限,但不能消除 TTL 内的脏读。

九、加强记忆

缓存一致性的本质是数据库和缓存两步操作无法原子。主流 Cache Aside 是读未命中查库回填,写先更库再删缓存。删除优于更新,因为删除幂等、避免并发旧值覆盖新值,也能省掉复杂缓存重算。延迟双删降低并发回填旧值概率,binlog 订阅和 TTL 负责最终一致兜底。