Redis与MySQL数据不一致:核心场景与解决方案

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_offsetslave_repl_offset),偏移量差值超过阈值时告警,暂停读从库。

方案5:最终一致性方案(生产级推荐)

通过监听MySQL binlog实现缓存的异步更新/删除,确保缓存与DB最终一致,是解决复杂场景不一致的最优方案。

核心实现:基于Canal/MaxWell监听binlog
  1. 部署Canal(阿里开源),模拟MySQL从库,实时监听MySQL binlog;
  2. Canal解析binlog后,将数据变更事件发送到MQ(如Kafka);
  3. 消费MQ消息,异步更新/删除Redis缓存;
  4. 增加重试机制和消息幂等性(如基于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实现最终一致性
读穿透并发回写冲突多请求回写新旧数据分布式锁控制缓存回写

四、生产最佳实践

  1. 避免缓存更新,只做缓存删除:更新缓存易引发并发覆盖,删除缓存让读请求自动加载最新数据更可靠;
  2. 设置合理的缓存TTL:即使出现不一致,TTL到期后缓存会自动失效,数据最终恢复一致(建议核心数据TTL 5~15分钟,非核心数据1~2小时);
  3. 监控告警:监控缓存命中率、缓存与DB数据不一致率(抽样对比关键key),异常时及时告警;
  4. 全量同步兜底:定期(如每天凌晨)全量同步MySQL数据到Redis,修复偶发的不一致问题。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TracyCoder123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值