欢迎光临
我们一直在努力

[Redis小技巧20]先删缓存还是先更新数据库?一文厘清 Redis 缓存一致性难题

在现代分布式系统中,Redis 几乎已成为缓存层的“标配”。然而,缓存与数据库之间的一致性问题,始终是高并发场景下的“阿喀琉斯之踵”。

一、为什么缓存一致性如此棘手?

缓存一致性问题的本质,源于写操作在缓存与数据库之间的非原子性。当多个线程/服务同时读写时,可能出现以下典型异常:

  • 脏读:读到旧缓存数据(数据库已更新,缓存未失效)
  • 幻读:缓存刚被删除,另一个请求回种了旧值
  • 雪崩写入:缓存失效瞬间大量请求穿透到 DB

这些问题在秒杀、订单、库存等强一致性场景中尤为致命。

二、主流缓存一致性方案详解

1. Cache-Aside(缓存分离)模式 —— 行业默认方案

这是最广泛采用的模式,其读写流程如下:

  • 读:先查缓存 → 未命中则查 DB → 回填缓存
  • 写:先更新 DB → 再删除缓存(注意:不是更新缓存!)

优势:简单、通用、避免缓存与 DB 写放大
风险:若“删缓存”失败,将导致长时间不一致

Cache-Aside 写流程
在这里插入图片描述

关键点:先更新 DB,再删缓存。若反过来(先删缓存再更新 DB),在并发场景下极易出现“旧值回种”问题。

2. “先删缓存,再更新 DB” —— 为何是反模式?

假设有两个并发请求:

  • 请求 A:先删缓存
  • 请求 B:读缓存(miss)→ 读 DB(旧值)→ 回种缓存
  • 请求 A:更新 DB(新值)
  • 结果:缓存中存的是旧值,DB 是新值 → 不一致!

    此方案在高并发下几乎必然失败,强烈不推荐。

    3. 双删(Double Delete)策略

    为缓解“旧值回种”,可在写操作中执行两次删除:

    1. 删除缓存
    2. 更新数据库
    3. 延迟 N 毫秒后,再次删除缓存

    第二次删除旨在清除步骤 2 期间可能被并发读请求回种的旧值。

    优点:比单删更可靠
    缺点:引入延迟、仍不能 100% 保证一致(若第二次删失败或延迟不足)

    双删失败案例展示

    (1)场景设定

    • 缓存键:user:1001
    • 初始值:{"name": "Alice", "balance": 100}
    • 有两个并发操作同时发生:
      • 写请求 W:将余额更新为 200(使用“双删”策略)
      • 读请求 R:在 W 执行过程中发起读取

    (2)详细时序(展示“双删”失败的情况)

    时间点操作状态
    T0 W 开始:第一次删除缓存 (DEL user:1001) 缓存为空
    T1 R 发起:读缓存 → miss
    T2 R 继续:读数据库 → 得到旧值 balance=100 DB 尚未更新
    T3 W 继续:更新数据库 → balance=200 DB 已更新
    T4 R 继续:将旧值 balance=100 回种到缓存 ❌ 缓存被污染!
    T5 W 等待 N 毫秒(比如 500ms)后:第二次删除缓存 缓存再次被清空
    T6 另一个读请求 R2:缓存 miss → 读 DB → 得到 200 → 回种正确值 ✅ 恢复正常

    问题出在哪里?

    • 在 T4 到 T5 之间,缓存中存储的是错误的旧值(100)。
    • 如果在这段时间内有其他读请求(比如 T4.5),它们会读到脏数据。
    • 更糟的是:如果第二次删除因网络超时、Redis 故障或程序异常而失败,这个错误值可能长期驻留缓存!

    (3)极端但真实的失败案例

    假设系统延迟波动大(如 GC 停顿、网络抖动),设置的“N = 500ms”可能不够:

    • 数据库主从同步延迟 800ms
    • 某个读请求从从库读到了旧值(因为主从未同步完成)
    • 它在 T4 回种了旧值
    • 而你的双删只等了 500ms,第二次删除发生在主从同步完成前,此时Redis缓存读到的依然是旧数据
    • 结果:缓存长期不一致

    (4) 如何缓解?——但无法根治

  • 增大延迟时间 N(如 1–2 秒)→ 但牺牲响应速度,用户体验下降
  • 第二次删除失败时重试(如最多 3 次)→ 增加复杂度
  • 结合 Binlog 监听(如用 Canal)自动失效缓存 → 转向更可靠的“订阅变更”模式
  • 但请注意:只要删除操作是“尽力而为”(best-effort),就无法提供强一致性保证。

    4. 延时双删(Delayed Double Delete)

    这是双删的增强版,通常配合消息队列实现:

    • 第一次删缓存(同步)
    • 更新 DB
    • 发送延迟消息(如 RabbitMQ / RocketMQ 延迟队列)
    • 消费者在 1–2 秒后执行第二次删除

    优势:解耦、可重试、避免阻塞主流程
    成本:需引入 MQ,系统复杂度上升

    5. 其他方案简述

    方案原理适用场景一致性强度
    Read/Write Through 缓存层代理所有读写,自动同步 DB 封闭系统、中间件可控
    Write Behind Caching 先写缓存,异步批量刷 DB 日志、监控等容忍丢失场景
    版本号/逻辑时钟 缓存与 DB 带版本,读时校验 极高一致性要求(如金融)

    注:Redis 本身不支持 Write Through,需应用层或代理层实现(如 Twemproxy + 自定义逻辑)。

    三、方案对比总表

    方案是否推荐并发安全性实现复杂度最终一致性典型应用场景
    先删缓存后更新 DB ❌ 否 ❌ 差
    Cache-Aside(删缓存+更新DB) ✅ 是 ✅ 可接受 通用 Web 应用
    双删 ⚠️ 谨慎 中高 ✅ 较好 订单、库存
    延时双删 ✅ 推荐(高并发) ✅ 优 电商、支付
    Read/Write Through ✅(若架构支持) ✅ 强 金融核心系统

    四、高频面试题

    Q1:为什么 Cache-Aside 模式要“先更新 DB,再删缓存”?

    答:若先删缓存,DB 更新前若有并发读请求,会将旧值重新加载到缓存,导致后续读取到脏数据。先更新 DB 可确保即使缓存 miss,读到的也是最新值。

    Q2:双删一定能解决一致性问题吗?

    答:不能 100% 保证。若第二次删除失败,或延迟时间不足以覆盖所有并发读,仍可能不一致。需配合重试、告警、监控机制。

    Q3:如何监控缓存一致性问题?

    答:可通过以下方式:

    • 记录缓存命中率突降
    • 对比 DB 与缓存的关键字段(抽样校验)
    • 使用 Redis 的 MONITOR(仅调试)或审计日志
    • 在业务层埋点记录“缓存版本 vs DB 版本”

    Q4:能否用 Redis 事务(MULTI/EXEC)保证一致性?

    答:不能。Redis 事务不支持回滚,且无法跨 DB 与缓存原子操作。缓存一致性本质是分布式事务问题,需更高层协调。

    赞(0)
    未经允许不得转载:171主机测评 » [Redis小技巧20]先删缓存还是先更新数据库?一文厘清 Redis 缓存一致性难题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址