延迟双删策略

延迟双删策略

什么是延迟双删

延迟双删(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
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容