跳到主要内容

缓存模式、更新顺序与一致性

缓存通过保存数据副本缩短读取路径。数据库和缓存同时保存状态后,任何更新都可能经历两次写入,因此需要明确权威数据、更新顺序、失败恢复和允许的不一致时间。

1. 先确认数据是否适合缓存

缓存适合读取频繁、计算或查询成本较高,并且能接受短暂旧值的数据。命中率很低、每次都必须读取最新状态,或者值变化远快于读取的数据,加入缓存未必有收益。

设计前先回答:

  • 权威数据存在哪里。
  • 最长可以接受多久的旧值。
  • 缓存丢失后能否从权威数据重建。
  • 单个 key 的大小、访问频率和过期方式。
  • 数据涉及余额、权限等高风险判断时,是否允许使用副本。

2. Cache-aside 由应用维护缓存

Cache-aside 是常见的读写方式:

读取:查缓存 → 未命中时查数据库 → 写入缓存
更新:更新数据库 → 删除缓存

缓存只保存可重建副本,数据库是权威状态。删除而不是直接更新缓存,可以让下一次读取按照统一逻辑重新加载,也能避免不同写入者计算缓存值的方式不一致。

TTL 是最后一道边界:即使失效通知失败,错误值也不会无限保留。但 TTL 只能限制最长陈旧时间,不能保证这段时间内读取正确。

3. 先更新数据库再删除缓存更容易收敛

如果先删除缓存,再更新数据库,删除与数据库提交之间的读请求可能读取旧数据库值,并重新填回缓存。数据库随后更新,但缓存里的旧值会持续到下一次失效。

先提交数据库,再删除缓存,常见执行路径会在删除后重新加载新值。它仍有失败窗口:数据库已经提交,但缓存删除失败,旧值会继续存在。

因此还需要:

  • 为缓存设置合理 TTL。
  • 对删除失败进行有限重试。
  • 通过事务 Outbox 或数据库变更日志可靠地产生失效事件。
  • 定期核对关键数据,发现长期不一致。

更新数据库和删除 Redis key 不属于同一个本地事务。代码把两行操作写在一起,并不会获得原子性。

4. 并发读写仍可能写回旧值

即使采用“更新数据库后删除缓存”,仍存在较窄的竞态:

  1. 读请求发现缓存未命中,并从数据库读取旧值。
  2. 写请求更新数据库并删除缓存。
  3. 前面的读请求最后才把旧值写回缓存。

这个窗口通常要求读请求在数据库写入前拿到旧值,却在失效后才回填,出现概率取决于查询延迟和并发量。可以根据一致性要求选择:

  • 缩短 TTL,让旧值尽快淘汰。
  • 回填时携带版本并只接受较新版本。
  • 对单个热点 key 合并加载,并在更新期间阻止旧回填。
  • 对关键读绕过缓存,直接读取权威存储。

不要为了一个低概率且低影响的旧值,引入无法验证的复杂分布式协议;也不要把余额、权限等错误后果较大的数据只交给 TTL。

5. Read-through 和 Write-through 改变责任位置

  • Read-through:缓存组件在未命中时负责加载数据,应用只读取缓存接口。
  • Write-through:写入缓存组件时同步写入权威存储,成功后再返回。
  • Write-behind:先更新缓存,再异步写入权威存储。

这些模式没有消除双写,只是把协调责任从业务代码移到缓存库或数据访问层。Write-behind 延迟低,但缓存故障、异步队列丢失和写入乱序都可能影响权威数据,只适合能接受相应风险的场景。

6. 多级缓存需要逐层失效

引入进程内缓存、Redis 和数据库后,同一数据存在三份状态。Redis key 删除成功,不会自动清除每个应用实例的本地缓存。

多级缓存通常需要失效广播、本地 TTL 和版本判断。实例重启、消息丢失或网络隔离后,仍要能通过 TTL 或版本重新收敛。层级越多,读取越快,但定位陈旧值来自哪一层也越困难。

7. 强一致读取应改变访问路径

如果一次业务判断必须读取刚刚提交的最新值,最直接的方式是读取权威数据库,或在同一个具备一致性保证的数据系统内完成原子检查与更新。

继续给 cache-aside 叠加延迟双删、锁和更多重试,只能缩小部分竞态窗口,不能让两个独立系统自动形成强一致事务。应根据错误成本决定哪些接口可以读缓存,哪些必须绕过缓存。

8. 用指标验证缓存是否有效

至少观察:

  • 命中率、请求量和回源量。
  • 各类 key 的大小、TTL 和淘汰原因。
  • 加载耗时、加载失败与单 key 并发。
  • 失效事件积压、失败与重试次数。
  • 数据版本差异和抽样核对结果。
  • 缓存故障时数据库增加的负载。

整体命中率可能掩盖某个关键接口持续未命中。指标应按业务、key 前缀和结果类型拆分。

9. 常见问题

9.1 为什么不直接同时更新数据库和缓存

并发写入可能以不同顺序到达缓存,例如数据库按 A、B 顺序提交,缓存却先写 B 再写 A,最终留下旧值。直接更新还要求每个写入入口都正确计算缓存内容。删除缓存让后续读取统一从数据库重建,状态更容易收敛。

9.2 延迟双删能保证一致性吗

不能。第二次删除可以覆盖部分旧值回填窗口,但延迟时间难以覆盖所有查询耗时和故障,删除本身也可能失败。它只能作为针对已验证竞态的补充措施,仍需 TTL、可靠失效和一致性边界。

10. 面试题

10.1 Cache-aside 为什么通常先更新数据库再删除缓存

出现公司:字节跳动、易点云

考察重点

  • 权威数据、缓存副本与 TTL 的作用。
  • 两种更新顺序的并发窗口。
  • 删除失败、旧值回填和强一致边界。

相关内容:第 2 节“Cache-aside 由应用维护缓存”至第 7 节“强一致读取应改变访问路径”。

参考回答

Cache-aside 以数据库为权威状态,读取未命中时回源并填充缓存,写入时先提交数据库再删除缓存。如果先删缓存,数据库提交前的并发读可能读取旧值并重新填回;先更新数据库通常更容易让下一次回源取得新值。

这个顺序仍不提供原子双写:删除可能失败,慢读也可能在失效后回填旧值。因此需要 TTL、删除重试、Outbox 或变更日志驱动的可靠失效,并根据错误成本决定是否增加版本校验。要求强一致的读取应绕过缓存或在权威存储中完成判断,不能只依靠延迟双删。