详解Redis数据库和缓存不一致的情况及解决方案

数据库与缓存不一致是分布式系统中常见问题,本质是数据在缓存层和存储层出现版本差异

一、并发写操作导致不一致(最常见)

  • 场景描述

    • 线程A更新数据库 → 线程B更新数据库 → 线程B更新缓存 → 线程A更新缓存

    • 结果 :缓存中存储的是线程A的旧数据

  • 发生条件

  • 解决方案

    • 分布式锁:对同Key的写操作加锁

    • 串行化队列:将写请求放入MQ顺序执行

二、先更新数据库后删除缓存失败(Cache-Aside模式)

  • 场景描述

    • 更新数据库成功

    • 删除缓存失败(网络抖动/Redis超时)

    • 结果 :缓存中残留旧数据

  • 关键代码风险点

    html 复制代码
    public void updateData(Data data) {
        db.update(data);          // 步骤1:数据库更新成功
        cache.delete(data.getId()); // 步骤2:缓存删除失败!
    }
  • 解决方案

    • 重试 机制

      java 复制代码
      void deleteWithRetry(String key, int maxRetry) {
          int retry = 0;
          while (!cache.delete(key) && retry++ < maxRetry) {
              Thread.sleep(50);
          }
      }
    • 异步补偿:通过Binlog监听(如Canal)触发二次删除

三、主从延迟导致脏读(读写分离架构)

  • 场景描述

    • 主库更新成功 → 删除缓存

    • 读请求从未同步的从库读取旧值 → 回填缓存

    • 结果 :缓存被旧数据污染

  • 解决方案

    • 延迟双删

      java 复制代码
      db.update(data);          // 更新主库
      cache.delete(key);        // 首次删除
      Thread.sleep(500);       // 等待主从同步
      cache.delete(key);        // 二次删除
    • 强制读主库:对一致性要求高的查询直连主库

四、缓存过期时高并发读(Cache Miss风暴)

  • 场景描述

    • 缓存过期瞬间涌入大量读请求

    • 请求1查DB → 请求2查DB → ... → 请求N查DB

    • 多个线程同时回填缓存(可能乱序)

    • 结果 :缓存可能被中间状态数据覆盖

  • 极端案例

    • 请求1读取到旧值V1,回填耗时久

    • 请求2读取新值V2并先完成回填

    • 请求1最终将V1写入缓存(覆盖V2)

  • 解决方案

    • 互斥锁重建:仅允许一个线程重建缓存

    • 逻辑过期:物理缓存永不过期,通过逻辑时间控制

五、批量操作与部分失效

  • 场景描述

    • 场景1:批量更新数据库成功,但部分缓存删除失败

    • 场景2:分页查询缓存无法感知单条数据变更

    • 结果 :缓存中存在部分脏数据

  • ​​​​​​​ 典型案例

    • 商品列表页缓存无法感知单个商品价格变更

    • 订单列表缓存未失效时,订单状态已更新

  • 解决方案

    • 缓存维度拆分:按查询条件设计缓存Key

    • 增量广播:通过消息队列通知相关缓存失效

    • 短过期时间:对聚合查询设置更短TTL

六、跨服务数据变更

  • 场景描述

    • 服务A更新数据库 → 服务B的缓存未失效

    • 原因:服务间缺乏缓存协同机制

    • 结果跨服务缓存残留旧数据

  • ​​​​​​​微服务常见问题

  • 解决方案

    • 领域事件通知:通过消息队列(Kafka/RabbitMQ)广播变更

    • 统一缓存层:所有服务通过SDK操作缓存,SDK统一处理失效

七、终极解决之道:取舍策略

根据业务需求选择合适的一致性级别:

策略 一致性强度 性能影响 适用场景
先删缓存再更新DB 写少读多
先更新DB再删缓存 通用方案(需重试)
双写+事务 金融交易
延迟监听(Binlog) 最终一致 高并发写场景
忽略不一致+短TTL 允许短暂脏读的业务

重要原则:不要尝试绝对强一致 ,除非接受性能断崖式下降。通常建议采用 "更新DB + 延迟双删 + 重试队列" 的组合策略。

相关推荐
一只爱撸猫的程序猿8 分钟前
构建一个结合AI功能实现智能的Redis哨兵切换检测和处理的简单实例
spring boot·redis·aigc
熙客17 分钟前
MongoDB:索引
数据库·mongodb
鸡窝头on20 分钟前
Redisson 自定义序列化 Codec 实战指南:原理、实现与踩坑记录
redis·后端
孟猛20231 小时前
Unix ODBC和Mysql ODBC
数据库
代码or搬砖1 小时前
Spring JDBC配置与讲解
java·数据库·spring
Ting-yu1 小时前
零基础学习Redis(14) -- Spring中使用Redis
redis·学习·spring
程序视点2 小时前
MySQL数据库操作完全指南:从入门到闭关
数据库·sql·mysql
远方16092 小时前
65-Oracle Undo机制
数据库·oracle
woai33642 小时前
首页实现多级缓存
redis·缓存·caffeine
Java知识库3 小时前
MySQL RC隔离级别惊现间隙锁:是bug吗?
数据库·mysql·bug