【Java 开发日记】
MySQL 与 Redis 如何保证双写一致性?(2026 年主流实践版)
在真实生产环境中,“双写一致性”几乎从来没有做到过强一致性(事务级原子性),绝大多数公司最终追求的都是最终一致性 + 可接受的不一致窗口。
下面按一致性强度从弱到强 + 复杂度从低到高排序,列出目前(2026年)最主流的几种方案,以及它们的适用场景、优缺点、残留风险和真实落地建议。
1. Cache-Aside(旁路缓存) + 先更新 DB,再删缓存(目前 80%+ 公司默认首选)
写流程:
1. 更新 MySQL
2. 删除 Redis key(不管成功与否都返回成功)
读流程:
1. 先读 Redis,命中 → 返回
2. 未命中 → 读 MySQL → 回写 Redis → 返回
为什么删缓存而不是更新缓存?
- 更新缓存需要把整个对象序列化回去(复杂对象特别麻烦)
- 并发场景下“谁后写谁覆盖”问题很难解决
- 删除更简单,失败了靠过期兜底
残留风险 & 概率排序(从高到低)
| 风险场景 | 概率(日常) | 不一致窗口 | 解决/缓解办法 |
|---|---|---|---|
| 更新 DB 成功,删 Redis 失败 | ★★★☆☆ | 直到 key 过期 | 加重试 + 记录失败 key 到延迟队列重试 |
| 读写并发(经典脏读案例) | ★★☆☆☆ | 几百 ms ~ 几秒 | 延迟双删(见方案2) |
| 主从读写分离 + 从库延迟 | ★★☆☆☆ | 主从延迟时间 | 延迟双删 + sleep 时间包含主从延迟 |
| 极端网络抖动/Redis 挂了 | ★☆☆☆☆ | 永久(直到重启) | 监控 + 告警 + 降级读 DB |
2026 真实建议:
绝大多数中型项目就只用这个 + 合理过期时间(1~30分钟,根据业务热点调整)已经够用。
加一个“删缓存失败重试表/队列” 就能覆盖 99% 的场景。
2. Cache-Aside + 延迟双删(最常见的加强版)
写流程:
1. 删除 Redis(第一次删)
2. 更新 MySQL(事务提交)
3. sleep(500ms ~ 2s) // 经验值:读接口平均耗时 + 主从延迟预估
4. 再次删除 Redis(第二次删)
为什么能大幅降低脏数据概率?
第二次删把“读-写-读”并发中,读请求在第一次删后、MySQL提交前回写的旧值删掉了。
缺点
- sleep 时间很难精确(主从延迟抖动、网络抖动)
- 增加了写接口的响应时间
2026 真实做法:
sleep 时间一般设 500ms ~ 1500ms,或者改成发延迟消息到 MQ(推荐):
// 伪代码
@Transactional
public void updateUser(UserVO vo) {
// 1. 先删(可选)
redis.delete(key);
// 2. 更新 DB
userMapper.update(vo);
// 3. 发延迟消息(RocketMQ / Kafka 延迟队列)
rocketMQTemplate.sendDelayMsg("delay-delete-topic", key, 1000); // 延迟1秒
}
// 消费者收到消息后再删一次
@RocketMQMessageListener(topic = "delay-delete-topic")
public void onDelayDelete(String key) {
redis.delete(key);
}
这样写接口不阻塞,还更可靠。
3. 基于 Binlog / CDC 的异步更新(目前大厂主流终极方案)
代表组件:Canal + MQ、Debezium、Maxwell、阿里云 DTS 等
流程:
- 业务只更新 MySQL(不碰 Redis)
- MySQL Binlog 订阅 → 解析变更 → 发 MQ(或直接推 Redis)
- 消费者收到变更 → 删除/更新对应 Redis key
优点
- 业务代码几乎无侵入(不用关心缓存)
- 天然支持读写分离、主从延迟
- 最终一致性窗口很小(通常秒级)
缺点
- 引入额外中间件,运维复杂度↑
- Binlog 订阅有延迟(几十 ms ~ 几秒)
- 需要处理幂等、顺序消费、断点续传
2026 主流组合推荐
| 规模 | 推荐方案 | 备注 |
|---|---|---|
| 小中型 | Cache-Aside + 延迟消息双删 | 性价比最高 |
| 中大型 | 先更新 DB + 删缓存 + 失败重试 | 加个重试表或死信队列 |
| 大型/高要求 | Canal / Debezium → RocketMQ/Kafka → 消费删缓存 | 最稳,业务无感 |
| 极致强一致 | Rockscache / 分布式事务 + 标记删除 | 极少数金融、对账场景 |
快速决策表(2026 年版)
| 业务特点 | 推荐方案 | 一致性级别 | 复杂度 |
|---|---|---|---|
| 读多写少、允许几秒不一致 | Cache-Aside + 先更新DB再删缓存 + 过期 | 最终一致 | ★☆☆☆☆ |
| 写比较频繁,有脏读投诉 | 延迟双删(sleep 或延迟MQ) | 最终一致(更好) | ★★☆☆☆ |
| 主从读写分离严重 | 延迟双删 + MQ延迟消息 | 最终一致 | ★★★☆☆ |
| 要业务无侵入、规模很大 | Binlog 订阅 → MQ → 删/更新缓存 | 最终一致(秒级) | ★★★★☆ |
| 金融级、对账必须几乎零差错 | Rockscache / 2PC + 版本号 / 标记删除 | 接近强一致 | ★★★★★ |
一句话总结(最实用版):
大多数项目:先更新数据库 → 删缓存 + 合理过期时间 + 删失败重试
进阶一点:把第二次删改成发延迟消息
再往上走:直接订阅 Binlog 做异步更新,业务代码彻底不管缓存
你当前项目属于哪一档?是已经有脏数据投诉了,还是想提前做预防?可以告诉我更多场景,我帮你选最合适的落地方案。