4. Cache-Aside 模式详解:读流程、写流程、为什么先更新数据库再删除缓存、延迟双删策略

Cache-Aside(旁路缓存)是生产环境中最常用的缓存一致性模式。应用层同时持有数据库与缓存两套存储,由业务代码显式控制读写路径。理解其读流程、写流程以及“先更新数据库再删除缓存”的决策逻辑,是掌握缓存一致性的基础。

4.1 读流程:Cache-Aside 的标准读取路径

读请求的核心原则是 “缓存命中即返回,未命中则回源数据库并回填缓存”。具体步骤如下:

  1. 查询缓存:应用根据 key 从 Redis 读取数据。
  2. 缓存命中:直接返回缓存数据,流程结束。
  3. 缓存未命中:从后端数据库(如 MySQL)查询完整数据。
  4. 回填缓存:将数据库查询结果写入 Redis,并设置合理的过期时间(TTL)。
  5. 返回数据:将数据返回给调用方。
设计要点:回填缓存时必须设置 TTL,这是兜底的一致性保障。即使后续更新流程出现异常,缓存也会在 TTL 过期后自动失效,触发重新加载。
Cache-Aside 读流程 ① 查询缓存 ② 缓存命中? ③ 直接返回 ④ 回源数据库 ⑤ 回填缓存+TTL ⑥ 返回数据 说明:虚线表示缓存未命中时的回源路径;TTL 是最终一致性兜底。

4.2 写流程:先更新数据库,再删除缓存

Cache-Aside 的写流程遵循 “先更新数据库,后删除缓存” 的顺序。这是业界公认的、在大多数场景下能兼顾性能与一致性的最佳实践。

步骤 操作 说明
1 更新数据库 执行 UPDATE / INSERT / DELETE 语句,确保持久化成功。
2 删除缓存 将对应的 Redis key 删除(del),而非更新缓存。
为什么是“删除”而不是“更新”缓存?
更新缓存是“写+写”操作,在并发写场景下容易产生脏数据覆盖。删除缓存则是“懒加载”策略——下次读请求未命中时,会从数据库拉取最新数据回填,天然避免了并发写导致的覆盖问题。

4.3 为什么先更新数据库再删除缓存?

这个顺序的选择是为了规避 并发读写下的脏数据问题。如果先删除缓存再更新数据库,会出现一个经典的时序漏洞:

  1. 线程 A 删除缓存 key。
  2. 线程 B 读请求发现缓存未命中,从数据库读取旧数据并回填缓存。
  3. 线程 A 更新数据库为新值。
  4. 结果:缓存中永久保留了旧数据,直到 TTL 过期。

而 先更新数据库再删除缓存 的时序中,即使有并发读,也只会出现短暂的缓存命中旧数据(在删除缓存之前),但删除操作会立即清除旧缓存,后续读请求必然回源数据库获取最新值。这个不一致窗口极短,通常只有几毫秒。

策略顺序 并发风险 不一致窗口 推荐度
先删缓存,后更新 DB 高:旧数据可能被回填 长(直到 DB 更新完成) ❌ 不推荐
先更新 DB,后删缓存 低:短暂命中旧缓存 极短(毫秒级) ✅ 推荐

4.4 延迟双删策略

“先更新数据库再删除缓存”在极端情况下仍存在一个隐患:删除缓存失败。如果第二步删除缓存因为网络超时、Redis 宕机等原因失败,缓存中就会残留旧数据。延迟双删(Delayed Double Delete)正是为了解决这个删除可靠性问题而设计的增强策略。

4.4.1 延迟双删执行流程

  1. 第一次删除缓存:在更新数据库之前,先尝试删除缓存(快速失败,不影响主流程)。
  2. 更新数据库:执行数据库写操作。
  3. 延迟一段时间:休眠几百毫秒(通常 500ms~1s,视业务读耗时而定)。
  4. 第二次删除缓存:再次删除缓存,确保在延迟期间可能被回填的旧数据也被清除。
延迟时间如何设定?
延迟时间应大于“读请求回源数据库 + 回填缓存”的总耗时。一般建议 500ms~1s。如果业务读耗时较长,可适当延长,但不宜超过 2s,否则会阻塞写线程。
延迟双删策略时序 时间 ① 第一次删除缓存 ② 更新数据库 ③ 延迟等待(500ms~1s) ④ 第二次删除缓存 延迟双删的作用 • 第一次删除:快速清理旧缓存 • 更新 DB:写入最新数据 • 延迟等待:给并发读回填窗口 • 第二次删除:清除回填的旧数据 效果:即使第一次删除失败, 第二次删除也能兜底, 将不一致窗口压缩到延迟时间内。

4.4.2 延迟双删的适用场景与代价

维度 说明
适用场景 对一致性要求较高、且删除缓存可能因网络抖动偶尔失败的场景。
性能代价 写线程需要阻塞等待延迟时间,会降低写吞吐量。延迟时间越长,代价越大。
替代方案 使用异步消息队列(如 RabbitMQ、Kafka)重试删除操作,避免阻塞写线程。
最终建议 如果业务能接受秒级不一致,优先使用“先更新 DB 再删缓存”+ 异步重试;只有对毫秒级一致有硬性要求时,才考虑延迟双删。
生产实践:大多数互联网业务采用“先更新数据库 + 删除缓存 + 异步重试删除”的组合。删除缓存失败时,将失败的 key 写入消息队列,由消费者异步重试删除,既保证了最终一致性,又避免了写线程阻塞。