跳到主要内容
约 6 分钟3 张卡

缓存一致性

缓存一致性处理缓存副本与权威数据之间的差异。本文负责读写顺序、旧值回填和失效恢复。

1. 面试核心​

先记住这几条,再读正文
  • Cache-aside 写入顺序。 Cache-aside 未命中时回源,写入时先提交数据库再删除缓存。卡 · Cache-aside
  • 旧值回填。 缓存删除成功后,较早读取的旧值仍可被慢请求回填。卡 · 旧值回填与版本水位
  • 陈旧时间。 TTL 从缓存写入时计时,旧值回填会重新开始陈旧窗口。卡 · 旧值回填与版本水位
  • 失效重试。 持久化的失效事件支持删除失败重试,Pub/Sub 断线期间的消息会丢失。卡 · 可靠失效事件
  • 读取新值。 数据库主库上的新查询可避开缓存旧值,已有 RR 快照仍保留旧版本。

2. 适合缓存的数据​

缓存适合频繁读取且允许短暂旧值的数据。回源成本越高,缓存命中的收益越明显。每次判断都必须使用当前状态时,应由权威存储完成操作。

设计时需要明确以下条件。

  • 数据库是权威来源,缓存保存可重建副本。
  • 接口约定可接受的旧值时间。
  • 记录每类 key 的大小、访问频率和 TTL。
  • 缓存丢失后,数据库有受控的重建容量。

3. Cache-aside​

Cache-aside 由应用负责缓存与数据库之间的读写。

读取:查缓存 → 未命中时查数据库 → 回填缓存
更新:提交数据库 → 删除缓存

缓存删除后,下一次读取按统一逻辑重建值。直接更新缓存则需协调多个写入者的到达顺序。

TTL 限制一次缓存副本的存活时间。过期后回源,也要确保数据库读取符合新鲜度要求。

4. 更新顺序​

先删缓存时,数据库仍保留旧值。并发读可在提交前取到旧值,并回填缓存。写事务随后提交,缓存就留下旧副本。

先提交数据库再删缓存,后续回源可读取已提交值。数据库已提交但删除失败时,需要继续投递失效事件。

  • 每个缓存副本设置符合业务要求的 TTL。
  • 删除操作限制单次重试次数与耗时。
  • 持久化事件保存后续重试依据。
  • 积压和长期版本差异触发监控告警。

数据库提交与 Redis 删除由两个系统执行。本地数据库事务只覆盖前者。

5. 并发竞态​

先提交再删除仍存在旧值回填竞态。

  1. 读请求未命中缓存,从数据库读到旧值。
  2. 写请求提交新值,随后删除缓存。
  3. 原读请求恢复执行,将旧值写入缓存。

窗口大小受数据库查询和进程暂停时间影响。处理方式取决于允许的旧值时间。

  • 短 TTL 缩短一次旧副本的保留时间。
  • 版本校验拒绝已经落后的回填。
  • 热点加载合并减少同时运行的读请求。
  • 关键判断由权威数据库完成原子检查与更新。

6. 旧值回填与版本水位​

TTL 从回填时刻开始计时。假设旧查询暂停 30 秒,恢复后以 60 秒 TTL 回填。数据库更新后的旧值窗口就能超过 60 秒。

只比较缓存值内的版本,也有删除后的空档。缓存被删除后,旧回填看不到任何新版本,仍会写入。

一种设计是独立保存最低可接受版本。失效事件原子提高该版本水位,并清除旧副本。回填脚本只接受版本不低于水位的数据。水位要保留到所有旧加载请求都已结束。

该方案还需限制加载寿命,并处理失效事件的延迟。它是业务协议,不能仅靠给缓存值加 version 字段完成。

7. 可靠失效事件​

失效事件应有持久化的投递依据。可在数据库事务中同时写入业务数据和 Outbox。提交后,消费者删除缓存并确认事件。删除同一 key 可重复执行,因此适合重试。

订阅 binlog 也需保存消费位点,并监控积压。事件乱序时,应避免用旧事件覆盖较新的版本水位。

Redis Pub/Sub 是至多一次投递。订阅端断线期间,消息不会保留给它重放。因此,单独使用 Pub/Sub 无法承担可靠失效队列。

8. Read-through 与 Write-through​

Read-through 由缓存组件负责未命中加载。Write-through 由组件同步写入权威存储。Write-behind 先接收缓存更新,再异步写回。

这些模式将协调责任交给数据访问组件。Write-behind 还需持久化队列、顺序控制和失败恢复。可接受异步落库时,才能采用该写入边界。

9. 多级缓存​

进程内缓存、Redis 和数据库形成多份状态。删除 Redis key 后,应用实例中的旧值仍需失效。

Redis 6.0 起支持服务端辅助客户端缓存跟踪。跟踪连接接收键失效通知。连接断开后,客户端需清空相关缓存或重新验证。

独立的本地缓存也需要失效广播与 TTL。每层都要保留故障后重新收敛的路径。

10. 强一致读取​

需要读取刚提交的新值时,应选择权威主库的新查询。MySQL RR 的既有快照仍可返回历史版本。读取副本还受复制延迟影响。

库存或余额检查可直接放入条件 UPDATE。检查与更新由同一个数据库操作完成,避免读写间的竞争。

延迟双删与重试能覆盖部分缓存竞态。跨系统强一致则需要完整的一致性协议。

11. 缓存指标​

指标应按接口和 key 前缀拆分。

  • 命中率与回源量反映缓存收益。
  • TTL 与副本版本反映旧值保留情况。
  • 加载耗时与并发量反映重建压力。
  • 失效积压与失败次数反映恢复进度。
  • 抽样核对用于发现长期不一致。

整体命中率很高时,个别接口仍可持续回源。缓存故障演练需同时观察数据库承载情况。

12. 常见问题​

12.1 直接更新的乱序​

数据库提交顺序与缓存写入顺序可以不同。例如数据库先提交 A 再提交 B,缓存却先写 B 再写 A。旧值 A 最后覆盖新值。失效后统一回源,可减少写入者之间的更新协调。

12.2 延迟双删的适用范围​

13 · 面试题卡片​

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

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

考察重点

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

相关内容:Cache-aside至强一致读取。

参考回答

先提交数据库,可让删除后的新回源读到新值。

先删缓存会让提交前的读请求回填旧值。

先提交再删除仍需处理删除失败和旧读回填。TTL 限制一次副本的寿命,可靠失效事件支持重试。严格业务判断应回到权威存储,并明确快照与读副本边界。

13.2 删除缓存后,旧查询为何还能回填旧值,版本字段足够吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:回填时序、TTL 起点和独立版本水位。

相关内容:旧值回填与版本水位。

参考回答

旧查询可在删除前读到旧值,删除后才回填。

TTL 此时重新计时,旧值窗口也被推迟。版本只存在缓存值内时,删除会连同版本一起清除。可独立保存版本水位,原子拒绝更旧回填。还需约定加载寿命和失效投递延迟。

13.3 数据库已提交但缓存删除失败,如何保证后续还能重试?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:Outbox、消费位点与幂等删除。

相关内容:可靠失效事件。

参考回答

业务数据与 Outbox 事件可在同一数据库事务提交。

消费者根据持久化事件重复删除缓存,成功后确认。binlog 方案则需要保存消费位点并监控积压。Pub/Sub 断线消息会丢失,适合通知而非独立的可靠依据。

14. 参考资料​