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 | 先更新缓存再更新 DB | DB 更新失败,缓存是新值 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 后其他客户端命令仍可插入队列
- 语法检查早,运行时错误晚: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 重试,业务无侵入
关联题目
关联知识
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 张卡,点击翻面