怎么解决Mysql和Redis双写不一致问题?
在后端面试中,只要简历上写了 Redis,这道题是 100% 必问的:“怎么保证缓存和数据库的数据一致性?”
很多同学上来就背八股文:“延时双删、加分布式锁…”
但在面试官眼里,这道题考的不是死记硬背,而是你有没有处理过真实的线上事故。
因为在生产环境中,细节才是魔鬼。今天我们就从“青铜”到“王者”,拆解 4 种方案,并揭秘 3 个连老鸟都容易踩的致命误区。
方案 1:先删缓存,再更新 DB(青铜)
这是很多新手的直觉反应:“先把旧缓存清了,再改数据库,下次读的时候不就读到新的了吗?”
错!这是个巨大的坑。 只要你的系统有一点并发量,这个方案就是个 Bug 制造机。
翻车现场还原
看看下面这个时序图,你就知道为什么不能用了:

结局:MySQL 是新的,Redis 是旧的。而且因为 Redis 里有数据,后续请求都不会去查库,这个脏数据会一直存在,直到过期。
方案 2:先更新 DB,再删缓存(黄金 – Cache Aside)
这是业界最常用的 Cache Aside Pattern。
逻辑:
- 先更新 MySQL。
- 更新成功后,删除 Redis 缓存。
为什么这个比方案 1 好?
虽然理论上它也存在并发问题,但在实际生产中,这种情况发生的概率极低。
因为“数据库写操作”通常比“缓存读写”慢得多。要触发 Bug,需要读请求在“写请求更新完 DB 但还没删缓存”的极短微秒级时间窗口内完成整个操作,这需要极其巧合的时间差。
适用场景:90% 的读多写少业务。
方案 3:延迟双删(钻石 – 高并发优化)
如果你无法容忍方案 2 中那“万分之一”的概率,或者你的数据库主从延迟比较大,延迟双删是性价比最高的方案。
核心逻辑:先删缓存 -> 更新 DB -> 休眠 N 毫秒 -> 再删缓存
为什么要删两次?
- 第一次删:为了腾地儿。
- 第二次删:为了把“在更新 DB 期间,其他读请求可能写入的脏数据”给清理掉。
生产级代码实现
这里有个细节:休眠时间怎么定?
经验公式:Sleep 时间 ≈ (主从同步延迟 + 读请求平均耗时) * 1.5
一般设置为 300ms – 500ms。
public void updateUser(User user) {
String key = "user:" + user.getId();
// 1. 第一次删除
redisTemplate.delete(key);
// 2. 更新数据库
userMapper.updateById(user);
// 3. 异步延迟第二次删除 (防止阻塞主线程)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 关键:给主从同步留出时间
redisTemplate.delete(key);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, executor);
}
方案 4:Canal + MQ(王者 – 金融级一致性)
如果你做的是金融、支付业务,连“应用层删缓存失败”都不能容忍,那就必须把应用层解耦。
原理:应用层只管写 MySQL,由 Canal 伪装成 MySQL Slave 监听 Binlog,投递到 MQ,消费者负责删缓存并自动重试。
深度思考:金融业务的“双标策略”
很多人问:“Canal 也有延迟,金融业务怎么能忍?”
这里有一个认知误区。真实的金融架构采用了双标策略:
| 链路类型 | 场景 | 策略 | 是否查缓存 |
|---|---|---|---|
| 交易链路 | 转账、扣款 | 强一致性 (ACID) | 绝对不查!直接悲观锁查 DB |
| 展示链路 | 查余额、账单 | 最终一致性 | 查缓存 |
结论:Canal + MQ 保证的是“展示链路”的数据最终一定是正确的,不会出现“删缓存失败”导致的永久脏数据。
避坑指南:3 个让你背 P0 事故的“隐形误区”
方案选对了就稳了吗?下面这三个坑,踩中一个就是生产事故。
误区 1:在事务(@Transactional)里面删缓存
这是 Spring 开发中最容易犯的低级错误!
错误代码
@Transactionalpublic void updateUser(User user) {
userMapper.updateById(user); // MySQL 还没 Commit!
redisTemplate.delete("user:1"); // Redis 先删了
// ... 方法结束,事务才 Commit}
后果:Redis 删了,DB 还没提交。读请求进来读到旧值写回 Redis,随后事务提交。Redis 永远是旧值。
修正方案:利用事务同步管理器
public void updateUser(User user) {
userMapper.updateById(user);
// 注册一个回调,确保事务提交成功后再删缓存
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
redisTemplate.delete("user:" + user.getId());
}
}
);
}
误区 2:针对“热点 Key”直接删缓存
对于微博热搜、秒杀库存这种 Top Hot Key,千万不能直接删!
删掉缓存的瞬间,几万个请求会直接击穿到 MySQL,数据库瞬间 CPU 100% 宕机。
修正:
针对超热点 Key,使用双写更新(允许短暂不一致)或分布式锁(只放一个线程回写)。
误区 3:迷信延迟双删的 Sleep 时间
Sleep(500ms) 只是一个经验值。如果网络抖动,主从延迟飙升到 1 秒,你睡 500ms 也是白搭。
记住:延迟双删只能降低不一致概率,无法根除。
总结一张表
| 方案 | 一致性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 先删缓存再更新 DB | ⭐ | 低 | 不推荐 (Bug 多) |
| 先更新 DB 再删缓存 | ⭐⭐⭐ | 低 | 90% 通用业务 |
| 延迟双删 | ⭐⭐⭐⭐ | 中 | 高并发、主从延迟大 |
| Canal + MQ | ⭐⭐⭐⭐⭐ | 高 | 核心资金业务 (展示层) |
以上关于怎么解决Mysql和Redis双写不一致问题?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 怎么解决Mysql和Redis双写不一致问题?

微信
支付宝