先更新数据库还是先删缓存
核心争论
在缓存与数据库的一致性方案中,争议最大的问题是:更新操作时,应该先操作数据库,还是先操作缓存?
这里有两种主要的操作:删除缓存和更新缓存。组合起来有四种可能的执行顺序:
| 顺序 | 策略 | 推荐度 |
|---|---|---|
| 先删缓存,再更新 DB | Delete Cache First | ⭐⭐ |
| 先更新 DB,再删缓存 | Delete Cache After DB | ⭐⭐⭐⭐ |
| 先更新 DB,再更新缓存 | Update Cache After DB | ⭐⭐ |
| 先更新缓存,再更新 DB | Update Cache First | ⭐ |
方案一:先删缓存,再更新数据库
执行流程
写操作:
1. 删除缓存中的 Key
2. 更新数据库
读操作(在步骤 1 和 2 之间发生):
1. 缓存未命中
2. 查询数据库(拿到旧数据)
3. 将旧数据写入缓存 ← 缓存中写入了旧数据!
问题:读写并发导致不一致
时间线:
T1: 写线程 → 删除缓存(成功)
T2: 读线程 → 缓存未命中 → 查数据库(拿到旧数据 V1)
T3: 写线程 → 更新数据库(V1 → V2)
T4: 读线程 → 将旧数据 V1 写入缓存
结果:缓存中是 V1(脏数据),数据库中是 V2(新数据)
适用场景
- 读多写少且写操作立刻发生时读请求概率低的场景
- 可以容忍短暂不一致
- 配合”延迟双删”使用
方案二:先更新数据库,再删缓存
执行流程
写操作:
1. 更新数据库
2. 删除缓存中的 Key
读操作(在步骤 1 和 2 之间发生):
1. 缓存命中(缓存中还是旧数据?不,但读操作拿到的是旧数据)
→ 不对,让我重新分析
问题场景分析
时间线(先写 DB 再删缓存):
T1: 写线程 → 更新数据库(V1 → V2)
T2: 写线程 → 删除缓存
T3: 读线程 → 缓存未命中 → 查数据库(拿到 V2)→ 写入缓存
但有一个极低概率的并发问题:
T1: 读线程 → 缓存未命中 → 查数据库(拿到 V1)
T2: 写线程 → 更新数据库(V1 → V2)
T3: 写线程 → 删除缓存(此时缓存中还没有数据)
T4: 读线程 → 将 V1 写入缓存
结果:缓存中是 V1(脏数据)
但这种情况要求:
1. 缓存刚好过期(缓存 miss)
2. 读请求在数据库查询完成之前触发写操作
3. 读请求先于写请求的”删缓存”操作完成
实际发生概率极低,需要满足的条件非常苛刻。
为什么不先更新缓存
如果改为”先更新数据库,再更新缓存”:
T1: 写线程 A → 更新数据库(V1 → V2)
T2: 写线程 B → 更新数据库(V2 → V3)
T3: 写线程 B → 更新缓存(V3)
T4: 写线程 A → 更新缓存(V2) ← 把 B 的更新覆盖了!
结果:数据库中是 V3,缓存中是 V2
先更新缓存的风险:多个并发写操作可能导致缓存的最终值和数据库不一致。这就是为什么删除缓存比更新缓存更安全——下次读操作会重新加载最新值。
方案三:先更新缓存,再更新数据库(不推荐)
写操作:
1. 更新缓存(成功)
2. 更新数据库(失败)
结果:缓存中是 V2,数据库中是 V1 → 永久不一致
这是最危险的方案——数据库也可能失败。一旦缓存在先更新成功而数据库更新失败,就会永久不一致。而且由于缓存中没有过期时间或过期时间较长,问题可能持续很久。
为什么推荐”先更新 DB,后删缓存”
理由一:数据库操作更可靠
数据库支持事务,可以回滚。先操作数据库,如果失败可以回滚或重试。而缓存操作通常不考虑回滚。
理由二:不一致窗口最小化
先删缓存再更新 DB:
不一致窗口 = 从删缓存到更新 DB 的时间(可能很长)
先更新 DB 再删缓存:
不一致窗口 = 从删缓存到下次读请求重建缓存(缓存 miss 的瞬间)
理由三:极端情况的概率极低
前面分析的那个极低概率的并发问题,在实际生产环境中的发生概率非常小,通常可以忽略。
理由四:符合”最终一致性”要求
即使发生了短暂的不一致,缓存被删除后,下一次读取会从数据库加载最新数据,自动恢复一致性。
异常处理
缓存删除失败怎么办
不管是先删还是后删,如果删除缓存的操作失败了,都会导致不一致。
解决方案:删除重试机制
1. 更新数据库
2. 删除缓存
3. 如果删除失败 → 将删除任务放入消息队列
4. 通过消费者异步重试删除
或者使用 canal + MQ 的 Binlog 监听方案:
MySQL Binlog → Canal → Kafka → 消费者 → 删除缓存
总结
各种策略的合理性排序:
先更新 DB 再删缓存 > 先删缓存再更新 DB(需配合延迟双删) > 先更新 DB 再更新缓存 > 先更新缓存再更新 DB
在实际工程中,先更新数据库,后删除缓存是最推荐的方案,也是 Cache Aside 模式的标准做法。它虽然在极端并发下存在微小的不一致可能,但概率极低,且配合缓存的过期时间可以自动恢复。如果有更高的一致性要求,可以加上延迟双删或 Binlog 监听作为补充。


暂无评论内容