延迟双删策略
什么是延迟双删
延迟双删(Delayed Double Delete)是一种在”先删缓存,再更新数据库”方案基础上,通过异步延迟再次删除缓存来保证最终一致性的策略。
为什么要用延迟双删
回顾”先删缓存,再更新数据库”的问题:
T1: 删缓存(成功)
T2: 读请求 → 缓存未命中 → 查数据库(拿到旧数据 V1)
T3: 更新数据库(V1 → V2)
T4: 读请求 → 将旧数据 V1 写入缓存 ← 脏数据
结果:缓存中是 V1,数据库中是 V2
核心矛盾在于:在删缓存和更新数据库之间的时间窗口内,如果有读请求进来,会把旧数据重新写回缓存。
延迟双删的解决思路是:在更新数据库后,等待一段时间再删除一次缓存,把这段时间内可能写入缓存的旧数据清除掉。
延迟双删的完整流程
延迟双删三步走:
第一步:先删除缓存
redis.del("key:1001")
第二步:更新数据库
db.update("UPDATE table SET value = V2 WHERE id = 1001")
第三步:延迟 N 毫秒后再次删除缓存
Thread.sleep(N) // 等待
redis.del("key:1001")
时序分析
延迟双删模式下,前面的并发问题被解决:
T1: 写线程 → 删除缓存(第一次删除)
T2: 读线程 → 缓存未命中 → 查数据库(拿到 V1)
T3: 写线程 → 更新数据库(V1 → V2)
T4: 读线程 → 将 V1 写入缓存 ← 旧的脏数据进来了
T5: 写线程 → 延迟 N 毫秒后再次删除缓存
T6: 后续读请求 → 缓存未命中 → 查数据库(拿到 V2)→ 正确数据
最终一致性得到了保证 ✓
延迟时长的确定
延迟时间 N 是延迟双删最关键的参数。N 应该大于”读请求将旧数据写入缓存”所需的时间。
计算公式
N > 读请求的平均耗时 + 合理的冗余
具体来说,N 应该满足:
N > 一次完整的读请求时间
= 缓存 miss → 查数据库 → 网络传输 → 写入缓存
经验值
| 场景 | 推荐延迟 | 说明 |
|---|---|---|
| 数据库和内网延迟低 | 500ms | 大多数同机房场景 |
| 数据库响应较慢 | 1000-2000ms | 复杂查询或跨机房 |
| 超高并发场景 | 3000-5000ms | 需要更大的安全边际 |
// Java 示例
public void updateData(String key, Object newValue) {
// 第一步:删除缓存
redisTemplate.delete(key);
// 第二步:更新数据库
database.update(key, newValue);
// 第三步:延迟 500ms 后再次删除缓存
Thread.sleep(500); // 注意:生产环境建议使用异步延迟
redisTemplate.delete(key);
}
生产环境实现
异步延迟删除
Thread.sleep() 会阻塞当前线程,不适合生产环境。正确做法是使用异步延迟任务:
// 方案一:使用线程池
executorService.schedule(() -> {
redisTemplate.delete(key);
}, 500, TimeUnit.MILLISECONDS);
# 方案二:使用 Celery / RQ 等异步框架
def delayed_delete(key):
"""延迟删除缓存的异步任务"""
time.sleep(0.5)
redis.delete(key)
# 主流程
def update_data(key, value):
redis.delete(key) # 第一次删除
db.update(key, value) # 更新数据库
delayed_delete.delay(key) # 异步延迟第二次删除
使用消息队列
更可靠的方式是借助 MQ 来保证第二次删除一定执行:
def update_data(key, value):
redis.delete(key) # 第一次删除
db.update(key, value) # 更新数据库
# 发送延迟消息
mq.send_delayed(
topic="cache-cleanup",
message={"key": key},
delay=500 # 500ms 后消费
)
# 消费者
def handle_cache_cleanup(message):
redis.delete(message["key"])
第二次删除失败怎么办
如果第二次删除缓存失败了,缓存中依然残留着旧数据。
解决方案:重试机制
def delete_with_retry(key, max_retries=3):
"""带重试的缓存删除"""
for i in range(max_retries):
try:
redis.delete(key)
return
except Exception:
if i < max_retries - 1:
time.sleep(0.1 * (i + 1)) # 退避
else:
# 最后尝试:放入重试队列
retry_queue.put(key)
兜底:缓存过期时间
最终极的兜底是给缓存设置合理的过期时间:
# 即使第二次删除失败,缓存也会在过期后自动恢复一致
redis.setex(key, 3600, old_value) # 最多不一致 1 小时
延迟双删的优缺点
优点
- 有效解决”先删缓存再更新 DB”的并发问题
- 实现相对简单
- 保证最终一致性
缺点
- 延迟时间难以精确确定(取决于数据库查询耗时)
- 第二次删除存在失败风险
- 写操作的整体延迟增加了 N 毫秒
- 不适用于写入频繁的数据(频繁删缓存影响缓存命中率)
何时使用延迟双删
适用场景:
✅ 读多写少
✅ 并发读取的场景
✅ 可以容忍短暂的不一致
✅ 需要比"先更新 DB 再删缓存"更强的保证
不适用场景:
❌ 写入非常频繁
❌ 对延迟敏感
❌ 数据库查询时间不确定(波动大)
总结
延迟双删核心思路:先删缓存 → 更新数据库 → 等一会再删一次。第二次删除的目的是清除在读请求在时间窗口内写入缓存的脏数据。
关键要点:
1. 延迟时间需要大于”读请求将旧数据写回缓存”的时间
2. 第二次删除建议异步执行(不阻塞写线程)
3. 需要配置重试机制防止第二次删除失败
4. 设置缓存过期时间作为最终兜底
延迟双删是对”先删缓存再更新 DB”方案的增强优化。相比于标准的 Cache Aside 模式,它提供了更强的最终一致性保证,但代价是增加了实现复杂度和写操作的延迟。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容