缓存一致性
缓存一致性处理缓存副本与权威数据之间的差异。本文负责读写顺序、旧值回填和失效恢复。
1. 面试核心
先记住这几条,再读正文- Cache-aside 写入顺序。 Cache-aside 未命中时回源,写入时先提交数据库再删除缓存。卡 · Cache-aside
- 旧值回填。 缓存删除成功后,较早读取的旧值仍可被慢请求回填。卡 · 旧值回填与版本水位
- 陈旧时间。 TTL 从缓存写入时计时,旧值回填会重新开始陈旧窗口。卡 · 旧值回填与版本水位
- 失效重试。 持久化的失效事件支持删除失败重试,Pub/Sub 断线期间的消息会丢失。卡 · 可靠失效事件
- 读取新值。 数据库主库上的新查询可避开缓存旧值,已有 RR 快照仍保留旧版本。
2. 适合缓存的数据
缓存适合频繁读取且允许短暂旧值的数据。回源成本越高,缓存命中的收益越明显。每次判断都必须使用当前状态时,应由权威存储完成操作。
设计时需要明确以下条件。
- 数据库是权威来源,缓存保存可重建副本。
- 接口约定可接受的旧值时间。
- 记录每类 key 的大小、访问频率和 TTL。
- 缓存丢失后,数据库有受控的重建容量。
3. Cache-aside
Cache-aside 由应用负责缓存与数据库之间的读写。
读取:查缓存 → 未命中时查数据库 → 回填缓存
更新:提交数据库 → 删除缓存
缓存删除后,下一次读取按统一逻辑重建值。直接更新缓存则需协调多个写入者的到达顺序。
TTL 限制一次缓存副本的存活时间。过期后回源,也要确保数据库读取符合新鲜度要求。
4. 更新顺序
先删缓存时,数据库仍保留旧值。并发读可在提交前取到旧值,并回填缓存。写事务随后提交,缓存就留下旧副本。
先提交数据库再删缓存,后续回源可读取已提交值。数据库已提交但删除失败时,需要继续投递失效事件。
- 每个缓存副本设置符合业务要求的 TTL。
- 删除操作限制单次重试次数与耗时。
- 持久化事件保存后续重试依据。
- 积压和长期版本差异触发监控告警。
数据库提交与 Redis 删除由两个系统执行。本地数据库事务只覆盖前者。
5. 并发竞态
先提交再删除仍存在旧值回填竞态。
- 读请求未命中缓存,从数据库读到旧值。
- 写请求提交新值,随后删除缓存。
- 原读请求恢复执行,将旧值写入缓存。
窗口大小受数据库查询和进程暂停时间影响。处理方式取决于允许的旧值时间。
- 短 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 延迟双删的适用范围
延迟双删只能覆盖两次删除之间的部分旧值回填。查询耗时超过预设延迟时,旧请求仍可在第二次删除后回填。删除失败也需要重试,因此还需可靠失效和 TTL。
13 · 面试题卡片
13.1 Cache-aside 为什么通常先更新数据库再删除缓存
出现公司:字节跳动、易点云
考察重点
- 权威数据、缓存副本与 TTL 的作用。
- 两种更新顺序的并发窗口。
- 删除失败、旧值回填和强一致边界。
相关内容:Cache-aside至强一致读取。
参考回答
先提交数据库,可让删除后的新回源读到新值。
先删缓存会让提交前的读请求回填旧值。
先提交再删除仍需处理删除失败和旧读回填。TTL 限制一次副本的寿命,可靠失效事件支持重试。严格业务判断应回到权威存储,并明确快照与读副本边界。
13.2 删除缓存后,旧查询为何还能回填旧值,版本字段足够吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:回填时序、TTL 起点和独立版本水位。
相关内容:旧值回填与版本水位。
参考回答
旧查询可在删除前读到旧值,删除后才回填。
TTL 此时重新计时,旧值窗口也被推迟。版本只存在缓存值内时,删除会连同版本一起清除。可独立保存版本水位,原子拒绝更旧回填。还需约定加载寿命和失效投递延迟。
13.3 数据库已提交但缓存删除失败,如何保证后续还能重试?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:Outbox、消费位点与幂等删除。
相关内容:可靠失效事件。
参考回答
业务数据与 Outbox 事件可在同一数据库事务提交。
消费者根据持久化事件重复删除缓存,成功后确认。binlog 方案则需要保存消费位点并监控积压。Pub/Sub 断线消息会丢失,适合通知而非独立的可靠依据。