Redis与MySQL数据不一致的核心根源是缓存与数据库的更新时序、并发竞争、异常中断,以及两者主从架构的延迟问题。以下是高频发生场景及对应的解决方案,覆盖基础规避策略到生产级最终一致性方案:
一、数据不一致的核心场景
场景1:写操作时序错误
1.1 先更新缓存,再更新数据库
发生场景:业务代码逻辑为「修改Redis缓存 → 修改MySQL」,是最基础的错误写法。
问题原因:更新MySQL时若发生异常(如网络中断、服务崩溃),会导致MySQL数据未更新,但Redis已写入新值,最终缓存与数据库数据不一致。
典型案例:用户余额修改,先把Redis中余额改为100元,再更新MySQL时服务宕机,MySQL仍为50元,Redis显示100元。
1.2 先删除缓存,再更新数据库(并发下仍会不一致)
发生场景:为避免“先更缓存再更DB”的问题,改为「删除Redis缓存 → 更新MySQL」,但高并发读场景下仍会出问题。
问题原因:
- 线程A删除缓存 → 线程A开始更新MySQL(耗时10ms);
- 同时线程B查询缓存,发现缓存不存在 → 从MySQL读取旧数据(未完成更新)→ 写入缓存;
- 线程A最终完成MySQL更新,此时Redis中是旧数据,MySQL是新数据。
场景2:高并发写冲突
发生场景:多个线程同时更新同一条数据(如秒杀库存、用户积分)。
问题原因:
- 线程1:更新MySQL(库存-1)→ 准备删除缓存;
- 线程2:更新MySQL(库存-1)→ 先于线程1删除缓存;
- 线程1完成删除缓存,线程2无操作;
- 最终MySQL库存正确,但缓存可能残留旧值(或被其他读请求回写旧值)。
场景3:缓存更新/删除失败
发生场景:更新MySQL后,执行Redis删除/更新操作时异常。
问题原因:
- Redis网络超时、宕机,导致
DEL/SET指令未执行; - 客户端连接池耗尽,无法向Redis发送指令;
- 大Key删除阻塞Redis主线程,指令执行超时。
场景4:主从延迟导致的不一致
4.1 MySQL主从延迟
发生场景:为分摊MySQL读压力,业务从MySQL从库读取数据并回写Redis。
问题原因:MySQL主库更新后,从库同步存在延迟(如1s),此时从库读取的是旧数据,回写Redis后导致缓存与主库数据不一致。
4.2 Redis主从延迟
发生场景:Redis开启主从架构,业务读取Redis从库数据。
问题原因:Redis主库更新(如删除缓存)后,从库同步延迟,读从库仍能获取到旧缓存数据。
场景5:异常中断(服务崩溃/网络分区)
发生场景:更新MySQL成功,但未执行缓存操作时,服务进程崩溃或网络断开。
问题原因:操作链路“更新MySQL → 更新/删除缓存”未执行完,缓存残留旧值,且无重试机制,导致长期不一致。
场景6:读穿透后的回写冲突
发生场景:缓存失效后,大量请求穿透到MySQL,并发回写缓存。
问题原因:
- 缓存过期,100个请求同时穿透到MySQL;
- 其中99个请求读取到旧数据(MySQL正在被更新),1个请求读取到新数据;
- 最终Redis可能被旧数据覆盖,导致不一致。
二、针对性解决方案
方案1:修正写操作时序(基础核心)
核心原则:「先更新数据库,再删除缓存」(而非更新缓存)
- 为什么不更新缓存?更新缓存会导致:① 写放大(频繁更新缓存);② 并发下新旧值覆盖;③ 无效更新(缓存未被读取却频繁更新)。
- 步骤:更新MySQL → 删除Redis缓存(让后续读请求重新从MySQL加载最新数据)。
补充:延迟双删(解决“先删缓存再更DB”的并发问题)
针对场景1.2的并发问题,在「先删缓存→更DB」基础上增加延迟二次删除:
// 伪代码
public void updateData(String key, Object newData) {
// 第一步:删除缓存
redis.del(key);
// 第二步:更新数据库
mysql.update(newData);
// 第三步:延迟N毫秒后再次删除缓存(关键)
executor.schedule(() -> redis.del(key), 500, TimeUnit.MILLISECONDS);
}
- 延迟时间:需大于MySQL更新耗时 + 业务读请求从DB加载数据的耗时(通常500ms~1s);
- 作用:清理掉并发读请求写入的旧缓存数据。
方案2:并发写控制(解决高并发冲突)
2.1 分布式锁
对更新同一条数据的操作加分布式锁(如Redis Redlock、ZooKeeper),确保同一时间只有一个线程执行“更新DB+删缓存”:
// 伪代码
public void updateData(String key, Object newData) {
// 获取分布式锁(key为数据唯一标识,如商品ID)
boolean lock = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (lock) {
try {
mysql.update(newData);
redis.del(key);
} finally {
redLock.unlock(); // 释放锁
}
}
}
2.2 乐观锁(MySQL层面)
在MySQL表中增加version字段,更新时校验版本号,避免并发覆盖:
-- 更新SQL(确保只有版本匹配时才更新)
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = 5;
- 业务层根据更新结果(影响行数)判断是否更新成功,失败则重试,确保数据准确性后再删缓存。
方案3:缓存操作失败重试(解决更新/删除失败)
3.1 本地重试 + 死信队列
- 本地重试:Redis操作失败时,本地重试3~5次(短间隔,如100ms);
- 死信队列:多次重试失败后,将“删除缓存”任务放入MQ死信队列(如RabbitMQ、RocketMQ),异步重试,确保最终执行成功。
3.2 降级兜底
Redis宕机时,暂时关闭缓存写入,所有读请求直接走MySQL,待Redis恢复后通过全量同步补全缓存。
方案4:解决主从延迟问题
4.1 MySQL主从延迟:强制读主库(核心数据)
- 对一致性要求高的数据(如订单、库存),更新后短期内(如1s)强制读MySQL主库,避免从库旧数据回写缓存;
- 非核心数据:设置缓存TTL大于MySQL主从同步延迟(如同步延迟1s,TTL设为5s)。
4.2 Redis主从延迟:读主库 + 监控同步偏移量
- 核心业务读Redis主库,非核心业务读从库;
- 监控Redis主从同步偏移量(
INFO replication中的master_repl_offset和slave_repl_offset),偏移量差值超过阈值时告警,暂停读从库。
方案5:最终一致性方案(生产级推荐)
通过监听MySQL binlog实现缓存的异步更新/删除,确保缓存与DB最终一致,是解决复杂场景不一致的最优方案。
核心实现:基于Canal/MaxWell监听binlog
- 部署Canal(阿里开源),模拟MySQL从库,实时监听MySQL binlog;
- Canal解析binlog后,将数据变更事件发送到MQ(如Kafka);
- 消费MQ消息,异步更新/删除Redis缓存;
- 增加重试机制和消息幂等性(如基于binlog offset去重),避免重复操作。
优势:
- 解耦:业务代码无需关注缓存操作,仅需更新MySQL;
- 可靠:binlog是MySQL的持久化日志,不会丢失,确保缓存最终同步;
- 高性能:异步操作不阻塞业务主线程。
方案6:读穿透回写控制(解决并发回写冲突)
对读穿透后的缓存回写操作加分布式锁,确保同一key只有一个请求回写缓存,其他请求等待:
// 伪代码
public Object getData(String key) {
// 第一步:查缓存
Object data = redis.get(key);
if (data != null) {
return data;
}
// 第二步:加锁,只有一个请求能回写缓存
boolean lock = redis.setnx(key + "_lock", "1", 3, TimeUnit.SECONDS);
if (lock) {
try {
// 再次查缓存(防止加锁期间其他请求已回写)
data = redis.get(key);
if (data == null) {
// 从MySQL读最新数据
data = mysql.query(key);
// 写入缓存(设置合理TTL)
redis.set(key, data, 30, TimeUnit.MINUTES);
}
return data;
} finally {
redis.del(key + "_lock");
}
} else {
// 未获取锁,休眠后重试
Thread.sleep(50);
return getData(key);
}
}
三、场景-解决方案对应表
| 不一致场景 | 核心原因 | 推荐解决方案 |
|---|---|---|
| 先更缓存再更DB | 操作时序错误,DB更新异常 | 改为「先更DB,再删缓存」 |
| 先删缓存再更DB(并发读) | 并发读请求回写旧数据 | 延迟双删 + 读穿透加锁 |
| 高并发写冲突 | 多线程操作顺序错乱 | 分布式锁 + MySQL乐观锁 |
| 缓存删除/更新失败 | 网络/Redis异常导致指令未执行 | 本地重试 + 死信队列 + MQ异步重试 |
| MySQL主从延迟回写缓存 | 从库读取旧数据 | 核心数据读主库 + 缓存TTL大于同步延迟 |
| Redis主从延迟读旧数据 | 从库同步未完成 | 核心业务读主库 + 监控同步偏移量 |
| 服务崩溃导致缓存未更新 | 操作链路中断 | Canal监听binlog实现最终一致性 |
| 读穿透并发回写冲突 | 多请求回写新旧数据 | 分布式锁控制缓存回写 |
四、生产最佳实践
- 避免缓存更新,只做缓存删除:更新缓存易引发并发覆盖,删除缓存让读请求自动加载最新数据更可靠;
- 设置合理的缓存TTL:即使出现不一致,TTL到期后缓存会自动失效,数据最终恢复一致(建议核心数据TTL 5~15分钟,非核心数据1~2小时);
- 监控告警:监控缓存命中率、缓存与DB数据不一致率(抽样对比关键key),异常时及时告警;
- 全量同步兜底:定期(如每天凌晨)全量同步MySQL数据到Redis,修复偶发的不一致问题。

被折叠的 条评论
为什么被折叠?



