3. Cache-Aside 模式详解:读流程、写流程、延迟双删策略、先删缓存后写库 vs 先写库后删缓存
Cache-Aside(旁路缓存)是分布式系统中最常用的缓存读写模式。应用层直接管理缓存的加载与失效,数据库作为权威数据源(Source of Truth)。理解其读/写流程、延迟双删策略以及两种写顺序的差异,是治理缓存一致性的基本功。
3.1 标准读流程(Cache-Aside Read)
读操作遵循“先查缓存,未命中则回源数据库并回填缓存”的原则,具体步骤:
- 查缓存:应用根据 Key 查询缓存(如 Redis)。
- 缓存命中:直接返回缓存数据,流程结束。
- 缓存未命中:从数据库查询完整数据。
- 回填缓存:将数据库查询结果写入缓存,并设置合理的 TTL(过期时间)。
- 返回数据:将数据返回给调用方。
关键点:读流程天然不会产生数据不一致,因为数据总是从数据库加载到缓存。不一致问题主要发生在写流程中。
3.2 标准写流程与两种顺序之争
写流程的核心矛盾在于:更新数据库与操作缓存的顺序。业界主要有两种方案,各有优劣。
| 方案 | 操作顺序 | 核心风险 | 适用场景 |
|---|---|---|---|
| 先删缓存,后写库 | ① 删除缓存 → ② 更新数据库 | 写库完成前,并发读可能回填旧数据到缓存 | 对一致性要求不极端,可接受短暂不一致 |
| 先写库,后删缓存 | ① 更新数据库 → ② 删除缓存 | 写库成功后、删缓存前,并发读仍读到旧缓存 | 大多数业务场景的首选,配合延迟双删更安全 |
3.2.1 先删缓存后写库(Cache Invalidate First)
流程:
- 应用删除缓存中的 Key。
- 应用更新数据库。
问题场景:在步骤 1 删除缓存后、步骤 2 提交数据库事务前,另一个并发读请求发现缓存未命中,从数据库读取到旧数据并回填到缓存。此时缓存中又变成了旧数据,而数据库即将被更新为新值,导致长期不一致。
避坑:此方案在并发写密集的场景下,缓存“脏数据”窗口期较长。通常需要配合“延迟双删”或“队列串行化”来补救。
3.2.2 先写库后删缓存(Database First)
流程:
- 应用更新数据库。
- 应用删除缓存中的 Key。
问题场景:在步骤 1 更新数据库后、步骤 2 删除缓存前,如果有并发读请求,会命中缓存中的旧数据。但这个窗口期极短(通常毫秒级),且缓存一旦删除,后续读请求会拉取新数据。
推荐实践:绝大多数业务场景采用“先写库后删缓存”。该方案的不一致窗口期最短,且不会出现“旧数据永久驻留缓存”的极端情况。
3.3 延迟双删策略(Delayed Double Delete)
延迟双删是对“先删缓存后写库”方案的增强,用于解决并发读回填旧数据的问题。核心思想是:在第一次删除缓存并完成数据库更新后,延迟一小段时间再次删除缓存。
完整流程:
- 第一次删除缓存:删除待更新 Key 的缓存。
- 更新数据库:执行数据库写操作。
- 休眠延迟:线程休眠一段时间(通常 500ms ~ 2s,需大于并发读回填缓存的时间)。
- 第二次删除缓存:再次删除该 Key 的缓存,确保在休眠期间被并发读回填的旧数据被清除。
延迟时间设定:延迟时间应大于“读请求从数据库拉取数据 + 写入缓存”的最大耗时。一般建议设置为 1~2 秒,或根据业务监控动态调整。
3.4 两种写顺序 + 延迟双删对比总结
| 策略 | 不一致窗口 | 实现复杂度 | 推荐度 |
|---|---|---|---|
| 先删缓存后写库 | 较长(并发读回填旧数据) | 低 | 低(需配合延迟双删) |
| 先写库后删缓存 | 极短(毫秒级) | 低 | 高(首选) |
| 先删缓存后写库 + 延迟双删 | 极短(第二次删除后) | 中(需异步任务或延时队列) | 中(适用于写冲突极高场景) |
3.5 Cache-Aside 写流程与延迟双删架构图
下图展示了“先写库后删缓存”的标准流程,以及“先删缓存后写库 + 延迟双删”的增强流程对比。
3.6 实战建议与避坑清单
- 优先选择“先写库后删缓存”:实现简单,不一致窗口最短,适合绝大多数业务。
- 延迟双删的异步化:第二次删除缓存建议通过异步消息队列或延时任务执行,避免阻塞主线程。
- 删除失败处理:缓存删除操作可能失败(如 Redis 超时),应加入重试机制(如本地重试 3 次,或写入删除失败日志由补偿任务处理)。
- 设置合理的 TTL:即使出现不一致,缓存的 TTL 可以作为最终一致性兜底。建议 TTL 不超过 1 小时。
- 避免“先删缓存后写库”裸用:如果不加延迟双删或队列串行化,高并发下极易出现缓存永久不一致。
最终结论:Cache-Aside 模式的最佳实践是“先写库后删缓存 + 删除失败重试 + 合理 TTL”。对于极端写冲突场景,可升级为“先删缓存后写库 + 延迟双删(异步)”。没有银弹,只有根据业务容忍度选择最合适的策略。