Skip to content

Latest commit

 

History

History
107 lines (86 loc) · 5.81 KB

File metadata and controls

107 lines (86 loc) · 5.81 KB

Redis缓存和MySQL数据如何做到一致性?

一、 基础认知:更新缓存 vs 删除缓存

在修改数据库数据时,对于缓存是选择更新(Update)还是删除(Delete)

  • 更新缓存(不推荐): 每次更新数据库时,也去更新缓存中的值。
    • 缺点:
      1. 并发写冲突: 线程A和线程B同时写入,DB先A后B,缓存先B后A,导致数据不一致。
      2. 计算开销: 如果写入频繁但读取很少,或者缓存的值需要经过复杂计算才能得到,那么每次写入都更新缓存是资源的浪费。
  • 删除缓存(推荐): 更新数据库时,直接把缓存删掉,等待下一次读取时由读请求去回填缓存(Lazy Loading)。
    • 优点: 简单,避免了复杂的计算和部分并发问题。

二、 进阶策略:操作顺序的博弈

既然选择了“删除缓存”,那么是先删除缓存,还是先更新数据库

方案 1:先删除缓存,再更新数据库

  • 流程:
    1. 线程 A 删除 Redis 缓存。
    2. 线程 A 更新 MySQL。
  • 问题(高并发下的脏数据): 如果在线程 A 删除缓存后,更新 MySQL 之前,线程 B 进来读取数据。
    • 线程 B 发现缓存空,去读 MySQL(此时还是旧数据)。
    • 线程 B 把旧数据写入 Redis。
    • 线程 A 终于将新数据写入 MySQL。
    • 结果: MySQL 是新数据,Redis 是旧数据,出现不一致。

方案 2:先更新数据库,再删除缓存(Cache Aside Pattern 标准模式)

  • 流程:
    1. 线程 A 更新 MySQL。
    2. 线程 A 删除 Redis 缓存。
  • 问题(极端情况下的脏数据): 虽然概率极低,但理论上存在:
    • 缓存刚好失效。
    • 线程 B 读 MySQL(旧值)。
    • 线程 A 更新 MySQL(新值)。
    • 线程 A 删除缓存。
    • 线程 B 将读到的旧值写入缓存。
    • 注意: 这种情况发生的条件是“写数据库”的操作比“读数据库+写缓存”的操作还要快,这在实际系统中几乎不可能,因为写DB通常比读DB慢得多。
  • 真正的问题: 第二步删除缓存失败了怎么办? 如果 DB 更新成功,但 Redis 删除失败,那么 Redis 里存的就一直是旧数据。

三、 高级解决方案:保证最终一致性

为了解决上述方案中的边缘Case(特别是删除失败或并发读写),业界有以下几种成熟方案:

1. 延时双删(解决“先删缓存”的并发问题)

如果你非要采用“先删缓存”的策略,或者为了保险起见,可以使用延时双删。

  • 流程:
    1. 删除缓存。
    2. 更新数据库。
    3. 休眠一小段时间(比如 500ms)。
    4. 再次删除缓存。
  • 原理: 休眠的时间是为了确保在步骤2执行期间,如果有并发的读请求把脏数据写入了缓存,步骤4可以把这个脏数据再次干掉。
  • 缺点: 增加了接口的响应时间(吞吐量降低)。

2. 消息队列重试机制(解决“删除失败”问题)

针对“先更DB,后删缓存”中删除失败的情况。

  • 流程:
    1. 更新数据库。
    2. 删除缓存。
    3. 如果删除失败,将要删除的 Key 发送到消息队列(MQ)。
    4. 消费者监听 MQ,接收到 Key 后不断重试删除操作,直到成功。
  • 优点: 实现了重试逻辑,保证最终一定删除。
  • 缺点: 侵入业务代码,业务逻辑和重试机制耦合。

3. 订阅 Binlog 变更(业界最佳实践 - 异步解耦)

这是目前大厂最常用的方案(如使用阿里的 Canal 中间件)。

  • 流程:
    1. 业务代码只操作 MySQL,完全不管 Redis。
    2. MySQL 更新数据后产生 Binlog。
    3. Canal 伪装成 MySQL 的从节点,订阅并解析 Binlog。
    4. Canal 将解析后的数据变更发送到消息队列(Kafka/RabbitMQ)。
    5. 专门的服务消费 MQ,负责执行 Redis 的删除或更新操作。
  • 优点:
    • 完全解耦: 业务代码不需要关心缓存一致性。
    • 可靠性高: 依赖 MySQL 的 Binlog,只要数据落库,缓存最终一定会被修正。
  • 缺点: 系统架构变复杂了,引入了新的中间件。

四、 总结与对比表

策略 适用场景 一致性强度 复杂度 缺点
先更DB,后删缓存 中小型项目、低并发 极端情况下有脏数据;如果删缓存失败会有问题。
延时双删 读多写少、由于网络原因可能产生脏数据 中高 增加请求耗时。
订阅 Binlog (Canal) 大型项目、高并发、对一致性要求较高 高 (最终一致) 架构复杂,引入中间件维护成本。
强一致性 (读写锁) 金融级别、绝对不能错 性能极差,几乎退化为串行,不适合高并发缓存。

面试回答话术建议

在回答时,建议遵循以下逻辑:

  1. 定调: 首先说明,通常我们提到的一致性是指最终一致性
  2. 抛出标准方案: 最通用的方案是 Cache Aside Pattern(先更DB,后删缓存)。
  3. 指出漏洞: 主动指出这个方案存在“删除失败”的风险。
  4. 给出解决方案:
    • 简单方案:利用消息队列进行失败重试
    • 进阶方案(加分项):利用 Binlog + Canal + MQ 进行异步更新,实现业务解耦和高可靠性。
  5. 补充场景: 如果面试官问“必须要强一致性怎么办”,回答“使用分布式锁(Redisson ReadWriteLock)或者直接不走缓存读 DB”,但要强调这会牺牲性能。