本文目录导读:

- 核心结论:先更新 DB,后删缓存(Cache-Aside 模式)
- 为什么不建议“先删缓存,后更新 DB”?
- “先更新 DB,后删缓存”的并发风险与补救
- 最终方案建议(结合 PHP 实际场景)
- 特殊情况:删除缓存失败怎么办?
- 总结(决策表)
在 PHP 开发中,先删缓存还是后删缓存”的问题,90% 的情况下应该选择“先更新数据库,后删缓存”。
这涉及经典的缓存一致性策略,以下是详细的分析和最佳实践:
核心结论:先更新 DB,后删缓存(Cache-Aside 模式)
这是业界最推荐、最稳妥的方式。
- 步骤:
- 更新数据库(Update DB)。
- 删除缓存(Delete Cache)。
- 原理:如果数据库更新成功但删缓存失败,下次读取会从数据库加载最新数据并回填缓存,缓存会自动恢复一致,这种方式容错率最高。
为什么不建议“先删缓存,后更新 DB”?
这种顺序会导致严重的并发脏数据问题,即 Cache Aside 的经典坑:
- 场景:线程 A(写)先删除了缓存,线程 B(读)此时来查缓存,发现缓存没数据,于是去数据库查询。
- 时序问题:
- 线程 A 删除缓存。
- 线程 B 查询 DB,拿到旧数据(因为线程 A 还没更新完 DB)。
- 线程 B 把旧数据写回缓存。
- 线程 A 此时才更新 DB 为新数据。
- 结果:数据库是新数据,缓存是旧数据,缓存被污染,且可能持续很久。
“先更新 DB,后删缓存”的并发风险与补救
这个方案并非绝对完美,也存在极端情况下的竞态(Race Condition):
- 场景:
- 线程 A(读)缓存 Miss,查询 DB 得到旧数据。
- 线程 B(写)更新 DB 为新数据,并删除缓存。
- 线程 A 将之前查到的旧数据写回缓存。
- 风险:缓存再次被旧数据污染。
- 评判:这种情况发生的概率极低,因为它要求“读”线程的查询耗时比“写”线程的更新+删除耗时还长,且步骤 2 必须在步骤 1 和步骤 3 之间穿插执行。
为了规避这个极小概率问题,通常采用“延迟双删”策略:
- 步骤:
- 更新数据库。
- 删除缓存。
- 休眠极短时间(如 500ms,视业务而定)。
- 再次删除缓存。
- 作用:延迟操作可以补掉步骤 3 中“线程 A”写回的旧缓存。
最终方案建议(结合 PHP 实际场景)
推荐顺序:先写 DB,再删缓存,必要时加延迟双删。
- 注意:如果在同一个 PHP 请求中,你刚刚更新完 DB,紧接着又要查询这条数据,此时不建议直接查缓存(因为缓存已被删),建议直接查 DB 或从返回结果中获取,避免空缓存穿透。
特殊情况:删除缓存失败怎么办?
如果你的代码执行了 delete 命令,但 Redis 返回失败(如网络抖动),此时缓存是脏的。
- 应对方案:
- 重试机制:将删除失败的 Key 发送到消息队列(MQ),由消费者异步重试。
- 设置过期时间:即使删缓存失败,Redis 的
EXPIRE(过期时间)是兜底方案,只要过期时间合理,最终会达到最终一致性。
决策表)
| 策略 | 同步性 | 并发安全性 | 推荐指数 |
|---|---|---|---|
| 先更新 DB,后删 Cache | 最终一致 | 高(推荐) | ⭐⭐⭐⭐⭐ |
| 先删 Cache,后更新 DB | 最终一致 | 低(脏数据概率大) | ⭐ |
| 先更新 DB,后删 Cache + 延迟双删 | 最终一致 | 极高(极致优化) | ⭐⭐⭐⭐⭐(高并发核心数据) |
给 PHP 开发者的最终建议:写业务逻辑时,不要写“先删缓存再更新数据库”的代码,请无条件使用“先更新数据库,然后再删除缓存的 Key”,如果项目对一致性要求苛刻,就在删除后加一个 usleep() 或异步任务做二次删除。