核心答案

常见方案是 Cache Aside:读请求先查缓存,未命中再查数据库并回填;写请求先更新数据库,再删除缓存。它通常保证最终一致性,而不是严格强一致。

为什么是删除而不是更新缓存?

更新缓存可能依赖多张表计算,也可能被低频读取造成无效写入。删除缓存让下一次读取根据最新数据库状态重新构建,逻辑更简单。

并发窗口

即使先更新数据库再删除缓存,也可能出现旧读请求在删除之后把旧值重新写回缓存。实际系统会根据一致性等级选择过期时间、延迟二次删除、互斥重建或基于消息的失效通知。

消息与 Binlog 方案

写入数据库后发送消息删除缓存,需要处理消息发送失败和重复消费。更可靠的做法是使用事务消息、Outbox,或者订阅数据库 Binlog,把数据变更转成缓存失效事件。

如何选择方案?

  • 商品详情等读多写少场景:Cache Aside + 合理 TTL。
  • 库存、余额等强约束数据:数据库或专门一致性服务作为判断依据,不能只信缓存。
  • 大规模派生缓存:CDC/Binlog + 消息队列异步失效。
  • 热点 key:增加互斥重建、逻辑过期和限流,防止击穿。

面试回答建议

先说明业务需要的是强一致还是最终一致,再给出读写流程、失败补偿、监控指标和降级方案。脱离业务直接说“延迟双删就能保证一致”是不严谨的。

核心考点清单

  • 先确认业务需要强一致、最终一致,还是允许短暂脏读。
  • Cache Aside 写路径通常是更新数据库后删除缓存。
  • 删除失败要有可靠重试,可使用 Outbox、消息队列或订阅 Binlog。
  • 热点 Key 要防击穿,批量过期要防雪崩,无效查询要防穿透。
  • TTL 只能限制不一致时间,是兜底而不是一致性保证。

并发时序为什么重要

先删缓存再写数据库时,另一个线程可能在写库前读到旧值并回填。先写数据库再删缓存仍有极小窗口,但发生条件更苛刻,也更容易通过 TTL 和可靠删除补偿。延迟双删可以降低部分并发回填风险,但等待时间难以精确设置,不能代替可靠重试。

高频追问与参考回答

追问 1:为什么删除缓存而不是更新缓存?

删除是幂等操作,容易重试;直接更新还要处理并发写入顺序,并可能为不再读取的数据做无效维护。

追问 2:如何做到真正强一致?

旁路缓存天然适合最终一致。关键读可绕过缓存,或使用支持事务的一致性存储,但必须接受更高延迟或更低可用性。

追问 3:分布式锁能解决全部一致性问题吗?

不能。它能限制并发回源,却无法原子覆盖数据库提交与缓存删除,还要处理锁超时、进程暂停和故障切换。

机制全景图

下面把「缓存与数据库一致性如何保证?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["业务写入事实库"]
    A --> B["事务提交形成版本"]
    B --> C["删除/更新缓存"]
    C --> D["并发读回源与回填"]
    D --> E["可靠重试和对账收敛"]

完整链路:从输入到结果

沿着「业务写入事实库 → 事务提交形成版本 → 删除/更新缓存 → 并发读回源与回填 → 可靠重试和对账收敛」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 业务写入事实库

先明确数据库或事件日志是事实源,缓存只能从可恢复事实派生。

2. 事务提交形成版本

事务提交点给数据一个确定版本,失败时不能提前让缓存暴露未提交状态。

3. 删除/更新缓存

Cache Aside 常在提交后删除缓存,删除失败必须进入可观测重试而不是忽略。

4. 并发读回源与回填

并发慢查询可能在删除后回填旧值,版本校验、短 TTL 或延迟双删用于缩小窗口。

5. 可靠重试和对账收敛

消息、CDC 和周期对账让长期差异收敛,并明确最大陈旧时间与人工处置。

源码与实现定位

入口 阅读重点
数据库 commit/binlog 事实版本
缓存 value.source_version 旧值识别

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

UPDATE entity ...; COMMIT; DELETE cache:key;

固定慢回填、删缓存失败和乱序 CDC,测最大陈旧窗口。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「缓存/源版本差」为主基线,记录值应满足「记录活动/稳态基线」;同时保存 源版本与缓存版本差、删除/更新重试积压,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「数据库 commit/binlog」确认请求确实进入「事实版本」对应的实现,再沿「缓存 value.source_version」观察「旧值识别」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「缓存故障时无限回源压垮数据库」,并把单一变量逐级放大,直到「缓存/源版本差」越过「超过容量或 SLO」。随后再分别验证「旧事件晚到覆盖新版本」和「一致性修复无审计导致重复副作用」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「提交后删并可靠重试」,确认它能控制影响范围;第二轮应用「回填带版本条件」,验证核心链路恢复;最后落实「强一致读绕缓存」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「缓存/源版本差」回到「记录活动/稳态基线」、「端到端 P99」回到「小于预算」、「状态差异」回到「0」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
缓存/源版本差 记录活动/稳态基线 超过容量或 SLO 触发降级
端到端 P99 小于预算 突破预算 停止扩量
状态差异 0 任意非零 补偿并对账

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:用户资料更新后不同接口读到两个版本

写服务更新数据库后删除一个缓存 Key,但聚合服务维护另一份派生缓存且未收到事件。通过统一版本事件、所有缓存记录 source_version,并用对账扫描补发后,跨服务缓存才收敛。

失败模式 首要证据 第一处置动作
缓存故障时无限回源压垮数据库 源版本与缓存版本差 提交后删并可靠重试
旧事件晚到覆盖新版本 删除/更新重试积压 回填带版本条件
一致性修复无审计导致重复副作用 陈旧读取比例 强一致读绕缓存

发布与回滚检查点

  • 发布前:确认「数据库 commit/binlog」对应实现和上述配置在目标版本仍然有效,并保存「缓存/源版本差」基线。
  • 灰度中:同时观察 源版本与缓存版本差、删除/更新重试积压、陈旧读取比例;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「提交后删并可靠重试」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「缓存故障时无限回源压垮数据库」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
Cache Aside 通用读多写少 简单、按需加载 删除失败与并发回填窗口
写穿/写后同步 写后读必须快速一致 缓存及时更新 双写失败和顺序复杂
CDC/事件驱动 多缓存或多写入口 以提交日志可靠收敛 延迟、乱序和运维成本

选型至少带上 峰值流量、数据规模、依赖容量、一致性目标和故障预算,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

缓存一致性的目标不是追求抽象的“绝对同步”,而是定义事实源、可接受陈旧窗口、失败恢复和最终验证;不能容忍陈旧的操作应绕过缓存。

工程落地遵循:先定义 SLO 与不变量,再通过限流、隔离、异步和补偿保护核心链路。回答时直接引用「数据库 commit/binlog」、配置实验和事故数据,比复述固定模板更有说服力。