缓存一致性

redis 📚 learning cache-consistency · redis · cache · consistency · cache-aside · canal · binlog · dual-delete

TL;DR(30 秒扫完)

  • 6 大不一致场景:更新后删失败 / 删后更新失败 / 并发 race / 先删后更 / 先更后删 / 主从延迟
  • 推荐模式 Cache-Aside:读时回填 + 写时删缓存(删除比更新安全)
  • 延迟双删:更新 DB → 删缓存 → 延迟 500ms-1s → 再删一次(清并发读回填的脏值)
  • 强一致方案:Binlog 订阅(Canal)+ MQ 重试 / CDC / 事务性消息
  • 兜底:TTL 是最终一致性的安全网

关键结论

结论 A删除缓存比更新缓存安全——并发写时更新缓存会有旧值覆盖新值的 race
结论 B延迟双删的延迟值应覆盖主从同步周期(经验 500ms-1s)
结论 CMULTI/EXEC 无回滚无隔离,生产环境推荐用 Lua 脚本替代
结论 DRedis 原子性是架构级保证(单线程),MySQL 是协议级保证(undo log)

完整讲解(费曼四步)

STEP 1 · 概念

缓存一致性:DB 与缓存数据保持同步的机制。由于非原子性 + 并发,两者必然存在不一致窗口,需要选择强一致或最终一致方案。

STEP 2 · 大白话

外卖比喻:
  • 不一致:外卖 App 显示"有货",但后厨其实已经卖完了
  • Cache-Aside:用户点菜时先查 App(缓存),App 没有就跑去问后厨(DB),拿到后更新 App
  • 延迟双删:后厨更新库存后先清空 App,过 1 秒再清空一次(防止有人刚好在更新时查到旧值又写回 App)

STEP 3 · 底层

不一致的 6 大场景

#场景现象
1更新 DB 后删除缓存失败脏数据长期驻留到 TTL
2删除缓存后更新 DB 失败下次读回填旧值
3并发 race:读线程读旧值 → 写线程更新 DB 删缓存 → 读线程把旧值写回缓存脏数据驻留
4先删缓存再更新 DB删除后并发读回填旧值
5先更新缓存再更新 DBDB 更新失败,缓存是新值 DB 是旧值
6主从延迟主库写完从库未同步,读走从库拿旧值

四大经典模式对比

模式读流程写流程一致性适用
Cache-Aside(推荐)先读缓存,miss 读 DB 回填更新 DB → 删缓存最终通用
Read-Through缓存 miss 由缓存层回填更新缓存 → 缓存层写 DB较强高一致性需求
Write-Through直接读缓存同步写缓存 + DB强写少读多
Write-Behind直接读缓存异步批量写 DB弱允许丢失

为什么"删除缓存"优于"更新缓存"

  • 更新缓存:先算旧值可能覆盖新值,写放大且难收敛
  • 删除缓存:让下次读自然回填最新值,天然正确
  • 并发写场景下更新缓存必然有 race,删除没有

延迟双删的正确姿势

更新 DB → 删缓存 → 延迟 N ms → 再删一次
  • N 值经验:500ms-1s(覆盖一次主从同步周期)
  • 为什么是删两次:第一次删后可能并发读回填旧值,第二次删清掉这个脏数据
  • 实现:线程池延迟任务 / 消息队列延迟消息 / 定时任务

Redis 原子性三层次

层级机制说明
命令层单命令(INCR/SETNX/LPUSH)事件循环中串行执行
脚本层Lua 一次性编译+执行执行期间不接受其他命令
事务层MULTI/EXEC 批量入队EXEC 时顺序串行(无回滚)
MULTI/EXEC 的坑:
  • 无回滚:中间一条失败,其余继续执行
  • 无隔离:MULTI 后其他客户端命令仍可插入队列
  • 语法检查早,运行时错误晚:SYNTAX 错在入队时报错,类型错在 EXEC 时报错
  • 生产环境推荐用 Lua 脚本替代 MULTI

Redis vs MySQL 原子性对比

维度Redis SETNX 原子性MySQL 事务原子性
来源架构(单线程)协议(undo log)
失败处理无回滚回滚到快照
粒度单命令 / 脚本 / MULTI事务内所有 SQL
隔离性天然串行靠锁 + MVCC

强一致性方案(按侵入性从小到大)

  • Binlog 订阅(Canal):消费 MySQL binlog → MQ → 删缓存;最推荐,业务无侵入
  • CDC 工具:Debezium / Flink CDC,同上思路
  • 事务性消息:RocketMQ 半消息,DB 事务与消息发送原子
  • 分布式事务:Seata AT/TCC/最终事务;侵入大,不推荐
  • 锁住整个流程:读锁 + 写锁,性能差

工程实践清单

数据类型推荐方案
关键数据(订单、余额)Binlog + MQ 强一致
一般数据(商品、配置)Cache-Aside + 延迟双删 + TTL
只读数据(静态内容)Read-Through + 长 TTL
高频写场景Write-Through 减少 DB 压力

兜底策略

  • TTL 是最终一致性的安全网:即使前面都失败了,TTL 到期后自然恢复
  • TTL 设多长:业务可容忍的脏数据时间窗(订单 5min,商品详情 1h)
  • 监控不一致率:定时任务抽样对比 DB 与缓存,超阈值告警

STEP 4 · 简化

一句话总结:不一致根因是非原子性+并发(6 大场景),Cache-Aside 删除比更新安全,延迟双删覆盖主从周期,强一致用 Canal+MQ,TTL 兜底。 记忆口诀:
  • 6 场景:更新后删失败 / 删后更新失败 / 并发 race / 先删后更 / 先更后删 / 主从延迟
  • Cache-Aside:读回填 + 写删缓存
  • 延迟双删:500ms-1s 覆盖主从周期
  • 强一致:Canal + MQ > 事务消息 > Seata
  • 兜底:TTL 是安全网

常见误区

说"更新缓存更安全"
删除比更新安全,并发写时更新缓存会 race
说"MULTI/EXEC 有回滚"
无回滚无隔离,推荐 Lua 脚本
说"延迟双删延迟随便设"
应覆盖主从同步周期(500ms-1s)
说"Redis 和 MySQL 原子性一样"
Redis 是架构级保证,MySQL 是协议级保证
说"TTL 不需要,前面方案够"
TTL 是最终一致性的安全网,必须设

延伸追问

Canal 如何保证不丢消息?
offset 落盘 + 断点续传 + MQ 至少一次投递
延迟双删的 N 值怎么定?
主从延迟监控 P99 + 业务读放大系数;经验 500ms-1s
读写分离下如何避免读到旧值?
强制走主库读(有写会话);session sticky;读前等待 binlog 同步
Seata AT 模式原理?为什么侵入小?
undo_log + 全局锁 + 一阶段本地事务二阶段补偿
如何监控缓存不一致率?
定时抽样对比 DB 与缓存;埋点上报不一致次数;阈值告警
库存扣减场景如何保证一致性?
Redis Lua 原子扣减 + MQ 事务消息 + DB 最终落库 + 对账补偿

速查表

6 场景: 更新后删失败/删后更新失败/并发race/先删后更/先更后删/主从延迟
Cache-Aside: 读回填 + 写删缓存(推荐)
延迟双删: 500ms-1s 覆盖主从周期
强一致: Canal+MQ > 事务消息 > Seata(侵入大)
Redis 原子: 命令层/脚本层/事务层(MULTI 无回滚)
MySQL 原子: undo log + 锁 + MVCC
兜底: TTL 是安全网(订单 5min, 商品 1h)

Anki 候选卡片

Q: DB 与缓存不一致的 6 大场景?
A: 更新后删失败、删后更新失败、并发 race、先删后更、先更后删、主从延迟
Q: 为什么删除缓存优于更新缓存?
A: 并发写时更新缓存会有旧值覆盖新值的 race,删除让下次读自然回填
Q: 延迟双删的延迟值设多少?
A: 500ms-1s,覆盖一次主从同步周期
Q: Redis MULTI/EXEC 事务有回滚吗?
A: 无回滚无隔离,推荐用 Lua 脚本替代
Q: 强一致缓存的推荐方案?
A: Binlog 订阅(Canal)+ MQ 重试,业务无侵入

关联题目

  • 《什么情况下会出现数据库和缓存不一致的问题?》— 2026-09-20 Round 1 Q7, ⭐⭐⭐⭐☆(根因抓得准,缺 6 大场景系统列举)
  • 关联知识

    DB 与缓存不一致的根因是非原子性+并发,具体有 6 类场景,推荐 Cache-Aside+延迟双删
    外卖 App:显示'有货'但后厨已卖完。Cache-Aside 用户点菜先查 App,没有就跑去问后厨更新 App
    ✦ 记 忆 口 诀 ✦
    删除比更新安全,延迟双删 500ms-1s 覆盖主从同步
    关键可视化
    6 大不一致场景
    flowchart TB
      A["DB 与缓存不一致"] --> S1["更新后删缓存失败"]
      A --> S2["删后更新失败"]
      A --> S3["并发 race"]
      A --> S4["先删后更新"]
      A --> S5["先更后删"]
      A --> S6["主从延迟"]
    Cache-Aside 读写流程
    flowchart LR
      subgraph 读
        R1["读请求"] --> R2{"缓存命中?"}
        R2 -->|是| R3["返回缓存"]
        R2 -->|否| R4["读 DB"] --> R5["回填缓存"] --> R3
      end
      subgraph 写
        W1["写请求"] --> W2["更新 DB"] --> W3["删除缓存"]
      end
    延迟双删解决并发 race
    flowchart LR
      U["更新 DB"] --> D1["删缓存 1"]
      D1 --> W["等待 500ms-1s<br/>覆盖主从同步周期"]
      W --> D2["删缓存 2<br/>清并发读回填的脏值"]
      D2 --> E["完成"]
      R["并发读请求"] -.回填.-> D1
    知识关系

    ⬆️ 前置(Prerequisite)

    redis-single-thread

    🔄 延伸(Extends)

    暂无

    ⚡ 对比(Contrast)

    缓存三失效— 三失效是读侧问题(穿透/击穿/雪崩),一致性是写侧问题(DB 与缓存不同步)MySQL 事务 ACID— MySQL 原子性是协议级保证(undo log),Redis 是架构级保证(单线程);缓存一致性靠应用层协调
    🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面