缓存模式、更新顺序与一致性
缓存通过保存数据副本缩短读取路径。数据库和缓存同时保存状态后,任何更新都可能经历两次写入,因此需要明确权威数据、更新顺序、失败恢复和允许的不一致时间。
1. 先确认数据是否适合缓存
缓存适合读取频繁、计算或查询成本较高,并且能接受短暂旧值的数据。命中率很低、每次都必须读取最新状态,或者值变化远快于读取的数据,加入缓存未必有收益。
设计前先回答:
- 权威数据存在哪里。
- 最长可以接受多久的旧值。
- 缓存丢失后能否从权威数据重建。
- 单个 key 的大小、访问频率和过期方式。
- 数据涉及余额、权限等高风险判断时,是否允许使用副本。
2. Cache-aside 由应用维护缓存
Cache-aside 是常见的读写方式:
读取:查缓存 → 未命中时查数据库 → 写入缓存
更新:更新数据库 → 删除缓存
缓存只保存可重建副本,数据库是权威状态。删除而不是直接更新缓存,可以让下一次读取按照统一逻辑重新加载,也能避免不同写入者计算缓存值的方式不一致。
TTL 是最后一道边界:即使失效通知失败,错误值也不会无限保留。但 TTL 只能限制最长陈旧时间,不能保证这段时间内读取正确。
3. 先更新数据库再删除缓存更容易收敛
如果先删除缓存,再更新数据库,删除与数据库提交之间的读请求可能读取旧数据库值,并重新填回缓存。数据库随后更新,但缓存里的旧值会持续到下一次失效。
先提交数据库,再删除缓存,常见执行路径会在删除后重新加载新值。它仍有失败窗口:数据库已经提交,但缓存删除失败,旧值会继续存在。
因此还需要:
- 为缓存设置合理 TTL。
- 对删除失败进行有限重试。
- 通过事务 Outbox 或数据库变更日志可靠地产生失效事件。
- 定期核对关键数据,发现长期不一致。
更新数据库和删除 Redis key 不属于同一个本地事务。代码把两行操作写在一起,并不会获得原子性。
4. 并发读写仍可能写回旧值
即使采用“更新数据库后删除缓存”,仍存在较窄的竞态:
- 读请求发现缓存未命中,并从数据库读取旧值。
- 写请求更新数据库并删除缓存。
- 前面的读请求最后才把旧值写回缓存。
这个窗口通常要求读请求在数据库写入前拿到旧值,却在失效后才回填,出现概率取决于查询延迟和并发量。可以根据一致性要求选择:
- 缩短 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 或变更日志驱动的可靠失效,并根据错误成本决定是否增加版本校验。要求强一致的读取应绕过缓存或在权威存储中完成判断,不能只依靠延迟双删。