4. Cache-Aside 模式详解:读流程、写流程、为什么先更新数据库再删除缓存、延迟双删策略
Cache-Aside(旁路缓存)是生产环境中最常用的缓存一致性模式。应用层同时持有数据库与缓存两套存储,由业务代码显式控制读写路径。理解其读流程、写流程以及“先更新数据库再删除缓存”的决策逻辑,是掌握缓存一致性的基础。
4.1 读流程:Cache-Aside 的标准读取路径
读请求的核心原则是 “缓存命中即返回,未命中则回源数据库并回填缓存”。具体步骤如下:
- 查询缓存:应用根据 key 从 Redis 读取数据。
- 缓存命中:直接返回缓存数据,流程结束。
- 缓存未命中:从后端数据库(如 MySQL)查询完整数据。
- 回填缓存:将数据库查询结果写入 Redis,并设置合理的过期时间(TTL)。
- 返回数据:将数据返回给调用方。
设计要点:回填缓存时必须设置 TTL,这是兜底的一致性保障。即使后续更新流程出现异常,缓存也会在 TTL 过期后自动失效,触发重新加载。
4.2 写流程:先更新数据库,再删除缓存
Cache-Aside 的写流程遵循 “先更新数据库,后删除缓存” 的顺序。这是业界公认的、在大多数场景下能兼顾性能与一致性的最佳实践。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 更新数据库 | 执行 UPDATE / INSERT / DELETE 语句,确保持久化成功。 |
| 2 | 删除缓存 | 将对应的 Redis key 删除(del),而非更新缓存。 |
为什么是“删除”而不是“更新”缓存?
更新缓存是“写+写”操作,在并发写场景下容易产生脏数据覆盖。删除缓存则是“懒加载”策略——下次读请求未命中时,会从数据库拉取最新数据回填,天然避免了并发写导致的覆盖问题。
更新缓存是“写+写”操作,在并发写场景下容易产生脏数据覆盖。删除缓存则是“懒加载”策略——下次读请求未命中时,会从数据库拉取最新数据回填,天然避免了并发写导致的覆盖问题。
4.3 为什么先更新数据库再删除缓存?
这个顺序的选择是为了规避 并发读写下的脏数据问题。如果先删除缓存再更新数据库,会出现一个经典的时序漏洞:
- 线程 A 删除缓存 key。
- 线程 B 读请求发现缓存未命中,从数据库读取旧数据并回填缓存。
- 线程 A 更新数据库为新值。
- 结果:缓存中永久保留了旧数据,直到 TTL 过期。
而 先更新数据库再删除缓存 的时序中,即使有并发读,也只会出现短暂的缓存命中旧数据(在删除缓存之前),但删除操作会立即清除旧缓存,后续读请求必然回源数据库获取最新值。这个不一致窗口极短,通常只有几毫秒。
| 策略顺序 | 并发风险 | 不一致窗口 | 推荐度 |
|---|---|---|---|
| 先删缓存,后更新 DB | 高:旧数据可能被回填 | 长(直到 DB 更新完成) | ❌ 不推荐 |
| 先更新 DB,后删缓存 | 低:短暂命中旧缓存 | 极短(毫秒级) | ✅ 推荐 |
4.4 延迟双删策略
“先更新数据库再删除缓存”在极端情况下仍存在一个隐患:删除缓存失败。如果第二步删除缓存因为网络超时、Redis 宕机等原因失败,缓存中就会残留旧数据。延迟双删(Delayed Double Delete)正是为了解决这个删除可靠性问题而设计的增强策略。
4.4.1 延迟双删执行流程
- 第一次删除缓存:在更新数据库之前,先尝试删除缓存(快速失败,不影响主流程)。
- 更新数据库:执行数据库写操作。
- 延迟一段时间:休眠几百毫秒(通常 500ms~1s,视业务读耗时而定)。
- 第二次删除缓存:再次删除缓存,确保在延迟期间可能被回填的旧数据也被清除。
延迟时间如何设定?
延迟时间应大于“读请求回源数据库 + 回填缓存”的总耗时。一般建议 500ms~1s。如果业务读耗时较长,可适当延长,但不宜超过 2s,否则会阻塞写线程。
延迟时间应大于“读请求回源数据库 + 回填缓存”的总耗时。一般建议 500ms~1s。如果业务读耗时较长,可适当延长,但不宜超过 2s,否则会阻塞写线程。
4.4.2 延迟双删的适用场景与代价
| 维度 | 说明 |
|---|---|
| 适用场景 | 对一致性要求较高、且删除缓存可能因网络抖动偶尔失败的场景。 |
| 性能代价 | 写线程需要阻塞等待延迟时间,会降低写吞吐量。延迟时间越长,代价越大。 |
| 替代方案 | 使用异步消息队列(如 RabbitMQ、Kafka)重试删除操作,避免阻塞写线程。 |
| 最终建议 | 如果业务能接受秒级不一致,优先使用“先更新 DB 再删缓存”+ 异步重试;只有对毫秒级一致有硬性要求时,才考虑延迟双删。 |
生产实践:大多数互联网业务采用“先更新数据库 + 删除缓存 + 异步重试删除”的组合。删除缓存失败时,将失败的 key 写入消息队列,由消费者异步重试删除,既保证了最终一致性,又避免了写线程阻塞。