3. Cache-Aside 模式详解:读流程、写流程、延迟双删策略、先删缓存后写库 vs 先写库后删缓存

Cache-Aside(旁路缓存)是分布式系统中最常用的缓存读写模式。应用层直接管理缓存的加载与失效,数据库作为权威数据源(Source of Truth)。理解其读/写流程、延迟双删策略以及两种写顺序的差异,是治理缓存一致性的基本功。

3.1 标准读流程(Cache-Aside Read)

读操作遵循“先查缓存,未命中则回源数据库并回填缓存”的原则,具体步骤:

  1. 查缓存:应用根据 Key 查询缓存(如 Redis)。
  2. 缓存命中:直接返回缓存数据,流程结束。
  3. 缓存未命中:从数据库查询完整数据。
  4. 回填缓存:将数据库查询结果写入缓存,并设置合理的 TTL(过期时间)。
  5. 返回数据:将数据返回给调用方。
关键点:读流程天然不会产生数据不一致,因为数据总是从数据库加载到缓存。不一致问题主要发生在写流程中。

3.2 标准写流程与两种顺序之争

写流程的核心矛盾在于:更新数据库与操作缓存的顺序。业界主要有两种方案,各有优劣。

方案 操作顺序 核心风险 适用场景
先删缓存,后写库 ① 删除缓存 → ② 更新数据库 写库完成前,并发读可能回填旧数据到缓存 对一致性要求不极端,可接受短暂不一致
先写库,后删缓存 ① 更新数据库 → ② 删除缓存 写库成功后、删缓存前,并发读仍读到旧缓存 大多数业务场景的首选,配合延迟双删更安全

3.2.1 先删缓存后写库(Cache Invalidate First)

流程:

  1. 应用删除缓存中的 Key。
  2. 应用更新数据库。

问题场景:在步骤 1 删除缓存后、步骤 2 提交数据库事务前,另一个并发读请求发现缓存未命中,从数据库读取到旧数据并回填到缓存。此时缓存中又变成了旧数据,而数据库即将被更新为新值,导致长期不一致。

避坑:此方案在并发写密集的场景下,缓存“脏数据”窗口期较长。通常需要配合“延迟双删”或“队列串行化”来补救。

3.2.2 先写库后删缓存(Database First)

流程:

  1. 应用更新数据库。
  2. 应用删除缓存中的 Key。

问题场景:在步骤 1 更新数据库后、步骤 2 删除缓存前,如果有并发读请求,会命中缓存中的旧数据。但这个窗口期极短(通常毫秒级),且缓存一旦删除,后续读请求会拉取新数据。

推荐实践:绝大多数业务场景采用“先写库后删缓存”。该方案的不一致窗口期最短,且不会出现“旧数据永久驻留缓存”的极端情况。

3.3 延迟双删策略(Delayed Double Delete)

延迟双删是对“先删缓存后写库”方案的增强,用于解决并发读回填旧数据的问题。核心思想是:在第一次删除缓存并完成数据库更新后,延迟一小段时间再次删除缓存。

完整流程:

  1. 第一次删除缓存:删除待更新 Key 的缓存。
  2. 更新数据库:执行数据库写操作。
  3. 休眠延迟:线程休眠一段时间(通常 500ms ~ 2s,需大于并发读回填缓存的时间)。
  4. 第二次删除缓存:再次删除该 Key 的缓存,确保在休眠期间被并发读回填的旧数据被清除。
延迟时间设定:延迟时间应大于“读请求从数据库拉取数据 + 写入缓存”的最大耗时。一般建议设置为 1~2 秒,或根据业务监控动态调整。

3.4 两种写顺序 + 延迟双删对比总结

策略 不一致窗口 实现复杂度 推荐度
先删缓存后写库 较长(并发读回填旧数据) 低 低(需配合延迟双删)
先写库后删缓存 极短(毫秒级) 低 高(首选)
先删缓存后写库 + 延迟双删 极短(第二次删除后) 中(需异步任务或延时队列) 中(适用于写冲突极高场景)

3.5 Cache-Aside 写流程与延迟双删架构图

下图展示了“先写库后删缓存”的标准流程,以及“先删缓存后写库 + 延迟双删”的增强流程对比。

Cache-Aside 写流程与延迟双删对比 Cache-Aside 写流程:两种顺序与延迟双删 方案一:先写库后删缓存(推荐) ① 更新数据库 ② 删除缓存 Key ③ 后续读请求回源 DB ④ 回填新数据到缓存 不一致窗口:仅存在于②之前 窗口极短(毫秒级),风险可控 方案二:先删缓存 + 延迟双删 ① 第一次删除缓存 ② 更新数据库 ③ 休眠延迟(如 1s) ④ 第二次删除缓存 解决:并发读在②之后回填的旧数据 延迟时间需 > 读回填最大耗时

3.6 实战建议与避坑清单

  • 优先选择“先写库后删缓存”:实现简单,不一致窗口最短,适合绝大多数业务。
  • 延迟双删的异步化:第二次删除缓存建议通过异步消息队列或延时任务执行,避免阻塞主线程。
  • 删除失败处理:缓存删除操作可能失败(如 Redis 超时),应加入重试机制(如本地重试 3 次,或写入删除失败日志由补偿任务处理)。
  • 设置合理的 TTL:即使出现不一致,缓存的 TTL 可以作为最终一致性兜底。建议 TTL 不超过 1 小时。
  • 避免“先删缓存后写库”裸用:如果不加延迟双删或队列串行化,高并发下极易出现缓存永久不一致。
最终结论:Cache-Aside 模式的最佳实践是“先写库后删缓存 + 删除失败重试 + 合理 TTL”。对于极端写冲突场景,可升级为“先删缓存后写库 + 延迟双删(异步)”。没有银弹,只有根据业务容忍度选择最合适的策略。