延迟双删(Delayed Double Deletion) 是在 Cache-Aside 基础上,为了解决"并发读写导致缓存回填旧值"问题而设计的方案。
一、问题背景:为什么需要双删?
先看单删的问题场景(先删缓存,再更新DB):
text
时间点 线程A(写) 线程B(读)
t1 删除缓存
t2 查询缓存(未命中)
t3 查询数据库 → 得到旧值
t4 更新数据库为新值
t5 将旧值写入缓存 ← 缓存变脏!
即使你先更新DB再删缓存,也有类似风险:
text
时间点 线程A(写) 线程B(读)
t1 更新数据库
t2 查询数据库 → 得到旧值(因主从延迟等)
t3 删除缓存
t4 将旧值写入缓存 ← 缓存变脏!
延迟双删的核心思想:等一等,让并发的读请求都结束了,再把缓存清掉。
二、延迟双删的具体步骤
text
1. 先删除缓存
2. 更新数据库
3. 休眠/等待一段时间(比如 200ms~1s)
4. 再次删除缓存
用代码表示:
python
import time
def update_with_delayed_double_deletion(key, new_value):
# 第一次删缓存
redis.delete(key)
# 更新数据库
db.update(key, new_value)
# 延迟一段时间(关键!)
time.sleep(0.5) # 500ms,根据业务调整
# 第二次删缓存(把可能回填的旧值清掉)
redis.delete(key)
三、延迟时间怎么定?
这是最关键的参数,定太短没效果,定太长影响性能。
建议公式:
延迟时间 ≈ 主从同步延迟 + 业务读操作耗时 + 冗余缓冲
- 主从同步延迟:如果有主从架构,通常 100ms~500ms
- 业务读操作耗时:查询DB + 序列化 + 网络往返
- 冗余缓冲:再加 100~200ms 保险
实际建议:
- 无主从:100~300ms
- 有主从:300~800ms
- 高并发场景:可以动态调整,或根据监控数据优化
四、第二次删除失败怎么办?
如果第二次删缓存时 Redis 挂了或网络抖动,缓存还是会脏。
解决方案:引入异步重试
python
import threading
def update_with_retry(key, new_value):
# 第一次删 + 更新DB
redis.delete(key)
db.update(key, new_value)
# 延迟后异步执行第二次删除
def delayed_delete():
time.sleep(0.5)
try:
redis.delete(key)
except Exception:
# 失败则放入重试队列(MQ 或本地延迟队列)
retry_queue.put(key)
threading.Thread(target=delayed_delete).start()
更生产化的做法是用 消息队列 或 延迟任务框架(如 Celery、XXL-Job)来做第二次删除,确保可靠执行。
五、完整流程图
text
写请求到来
│
▼
┌─────────────┐
│ 删除缓存 │◄── 第一次删除,清掉旧值
└─────────────┘
│
▼
┌─────────────┐
│ 更新数据库 │
└─────────────┘
│
▼
┌─────────────┐
│ 延迟等待 │◄── 等并发读请求结束,旧值回填完成
└─────────────┘
│
▼
┌─────────────┐
│ 再次删除缓存 │◄── 第二次删除,清掉可能回填的旧值
│ (带重试) │
└─────────────┘
│
▼
结束
六、优缺点总结
| 优点 | 缺点 |
|---|---|
| 实现简单,不引入额外组件 | 延迟等待会阻塞写请求(或用异步) |
| 能有效降低缓存不一致概率 | 延迟时间不好精确估算 |
| 兼容现有 Cache-Aside 架构 | 极端高并发下仍可能有小概率不一致 |
七、适用场景
- 读多写少:写操作不频繁,延迟等待的开销可接受
- 允许秒级最终一致:不要求实时强一致
- 无主从或主从延迟可控:延迟时间能估算得准
八、一句话总结
延迟双删 = "先删缓存 → 更新DB → 等一会儿 → 再删一次",用"等一等"来覆盖并发读回填旧值的时间窗口。