先搞懂问题本质
为什么会有「数据一致性」问题?
因为我们系统里同时存在两个”仓库”:
| 仓库 | 特点 | 类比 |
|---|---|---|
| MySQL | 持久化,数据不丢,但慢 | 正式仓库(在郊区) |
| Redis | 内存存储,超快,但断电丢数据 | 前台展示柜(在门口) |
用户来买东西,先看展示柜(Redis),有货直接拿走,快!
展示柜没货,才去正式仓库(MySQL)取,慢但可靠。
问题来了:展示柜的货和仓库的货,怎么保持一致?
经典场景:下单扣库存
假设你在电商平台买了最后一件商品:
// 伪代码:下单流程 1. 查询库存 → 先查Redis(快) 2. 库存 > 0 → 允许下单 3. 扣减库存 → 先更新MySQL(持久化) 4. 删除/更新Redis缓存
听起来很合理?但魔鬼藏在细节里 👇
四大经典坑
💣 坑一:先更新数据库,再删缓存(最常见方案)
线程A:更新MySQL库存 = 99 线程A:删除Redis缓存 线程B:查询Redis(缓存已删)→ 查MySQL → 写入Redis = 99 ✅ 正常情况:没问题
但如果这样呢:
线程A:更新MySQL库存 = 99 线程A:删除Redis缓存 ← 此时宕机! 结果:MySQL=99,Redis=100(旧数据) 💥 数据不一致!
🐟 比喻:仓库已经卖出一件,但展示柜忘记更新,顾客看到的还是旧数量。
💣 坑二:先删缓存,再更新数据库
线程A:删除Redis缓存 线程B:查询Redis(缓存不存在)→ 查MySQL(旧值100)→ 写入Redis=100 线程A:更新MySQL库存 = 99 结果:MySQL=99,Redis=100 💥 更惨!
🐟 比喻:展示柜刚清空,还没来得及从仓库补货,另一个员工又把旧数量填回去了。
💣 坑三:双写(同时更新MySQL和Redis)
线程A:更新MySQL=99,更新Redis=99 线程B:更新MySQL=98,更新Redis=98 // 如果顺序错乱: 线程A先写MySQL,线程B先写Redis 结果:MySQL=98,Redis=99 💥 又不一致!
💣 坑四:缓存击穿 + 并发写
高并发下,缓存刚删除的瞬间,大量请求同时打到MySQL,然后同时往Redis写,写入顺序不保证,最终结果随机。
🐟 比喻:展示柜空了,100个顾客同时冲进仓库,每人拿了一个数字往展示柜上贴,最后贴上去的是哪个数谁也不知道。
主流解决方案
✅ 方案一:Cache Aside(旁路缓存)—— 最常用
口诀:读的时候加缓存,写的时候删缓存
读操作: 1. 查Redis → 有则返回 2. 没有 → 查MySQL → 写入Redis → 返回 写操作: 1. 更新MySQL 2. 删除Redis缓存(不是更新!)
为什么是删除而不是更新缓存?
因为更新缓存在并发下容易写入旧值,删除更安全——下次读的时候自然会从MySQL重新加载最新值。
✅ 方案二:延迟双删 —— 解决并发问题
1. 删除Redis缓存 2. 更新MySQL 3. 睡眠500ms(等其他线程的读操作完成) 4. 再次删除Redis缓存
第二次删除是为了清掉步骤1-2之间可能被其他线程写入的旧缓存。
🐟 比喻:展示柜清空 → 更新仓库 → 等所有顾客都看完 → 再清一次展示柜,确保没有旧数据残留。
✅ 方案三:消息队列 + 异步更新 —— 高可靠
更新MySQL → 发消息到MQ → 消费者异步删除/更新Redis
优点:解耦,即使Redis删除失败,MQ会重试。
缺点:有短暂的不一致窗口(最终一致性)。
✅ 方案四:Canal监听binlog —— 终极方案
MySQL binlog → Canal → 消息队列 → 删除Redis
Canal 是阿里开源的 MySQL binlog 监听工具,MySQL 数据一变,Canal 立刻感知,自动同步到 Redis。
🐟 比喻:仓库装了监控摄像头(Canal),一旦货物有变动,自动通知展示柜更新,完全自动化,人工不介入。
面试怎么答?
面试官问这个问题,其实想考察你三点:
| 考察点 | 你应该说 |
|---|---|
| 问题意识 | 先说清楚为什么会不一致(并发、宕机) |
| 方案掌握 | Cache Aside → 延迟双删 → MQ → Canal,按复杂度递进 |
| 业务判断 | 根据一致性要求选方案(强一致 vs 最终一致) |
标准答案模板:
「Redis和MySQL的一致性问题,核心是写操作时的缓存处理策略。我们通常采用Cache Aside模式:读时加缓存,写时删缓存而非更新缓存。对于高并发场景,可以加延迟双删来降低脏数据概率。如果对一致性要求更高,可以引入消息队列做异步补偿,或者用Canal监听MySQL binlog来实现自动同步。具体选哪种,要看业务对一致性的容忍度。」

