Skip to Content
面试题库Redis[面试题] 如何保证Redis缓存和数据库的数据一致性(双写一致性问题)?

📖 版块 A:知识讲解版 (温故知新)

  • 核心矛盾:数据库(DB)和缓存(Redis)是两个独立的系统,无法通过本地事务保证强一致性。在并发更新的场景下,很容易发生两边数据不一致的情况。

  • 为什么不建议“更新缓存”

    • 更新缓存(如每次更新 DB 也计算并更新 Redis)容易产生时序错乱。比如线程 A 先写 DB,线程 B 后写 DB,但 B 先更新缓存,A 后更新缓存。最终 DB 里是 B 的新数据,缓存里是 A 的旧数据。
    • 此外,有些缓存数据的计算成本很高,如果更新后很少被读,就是一种浪费(写多读少场景)。
  • 主流的“删除缓存”策略(Cache Aside 旁路缓存模式)

    • 先更新数据库,再删除缓存:最推荐的基础方案。只有在极端的读写并发下(读请求拿到了旧缓存刚好失效时的 DB 数据,并且在写请求更新完 DB 删完缓存后,读请求才把旧数据回写到缓存中)才会不一致,概率极低。
    • 先删除缓存,再更新数据库:不推荐。如果线程 A 删了缓存还没写完 DB,线程 B 发起读请求发现没缓存,去 DB 拿到旧数据并回写到 Redis,最终必然导致脏缓存。
  • 高阶兜底/补偿方案(实现最终一致性)

    1. 延迟双删(针对先删缓存再更 DB 的补救):A 写完 DB 后,休眠个几百毫秒,再删一次缓存,确保 B 写的脏缓存被干掉。
    2. 消息队列异步删除:更新 DB 后发消息到 MQ,利用 MQ 的重试机制确保缓存必被删除。
    3. Canal 订阅 Binlog:业务代码只管更新 MySQL,后台中间件伪装成 MySQL 从库监听 Binlog,发现数据变动后精准删除 Redis。业务解耦,零侵入。

🎤 版块 B:面试实战版 (高手逐字稿)

Q:你们的项目中是如何保证 Redis 缓存和 MySQL 数据库双写一致性的?

💬 高手逐字稿

关于缓存和数据库的双写一致性,我在设计架构时通常会先明确一个大前提:我们的业务是要求强一致性,还是允许最终一致性? 如果是金融交易那种必须强一致的场景,我根本不会把核心数据放到缓存里,直接走数据库配合分布式锁是最稳妥的。只要引入了 Redis,其实我们就是在用一定的强一致性去换取极高的吞吐量,所以我们追求的应当是最终一致性

在具体的更新策略上,我坚决反对“更新数据库后去更新缓存”,因为在并发写场景下,很容易由于网络延迟导致时序错乱,最终造成脏数据。我们的原则是只删除缓存,不更新缓存(让下一次读请求自己去加载)。

那么问题来了,先删缓存还是后删缓存? 如果在高并发下先删缓存再更新 DB,很容易出现一个经典的问题:线程 A 刚把缓存删了还没更新完 DB,线程 B 进来查询,发现缓存空了就去查 DB,拿到旧数据写回了 Redis,这就永远不一致了。有些人会用“延迟双删”来打补丁,也就是 A 睡几百毫秒再删一次,但这严重依赖超时时间的设定,不够优雅。

所以,我们项目中采用的是业界最标准的 Cache Aside 旁路缓存模式:先更新数据库,再删除缓存。 这种方案在 99% 的场景下都能保证一致性。只有一种极端情况:一个读请求刚巧遇到了缓存失效去查 DB,在还没回写缓存的时候,另一个写请求更新了 DB 并且删除了缓存,最后那个读请求把老数据塞回了 Redis。但因为读库操作远快于写库操作,这种苛刻的时序发生概率微乎其微。

为了给这个方案加上最后一层绝对的保险,也就是防止“删除缓存失败”导致的数据不一致,我们在生产环境中引入了异步补偿机制。 如果是常规项目,我会在更新 DB 后往 MQ 发一条消息,通过 MQ 的重试机制确保缓存一定被删掉。但在我们当前的高可用架构里,为了对业务代码完全解耦,我们引入了 Canal 组件。业务层只管写 MySQL,完全不感知 Redis;后端的 Canal 伪装成 MySQL 的从节点,监听 Binlog 的日志变更,一旦发现数据改变,就去 Redis 里精准擦除对应的 Key。这样既保证了最终一致性,又让业务代码极其干净。

最后更新于:
⏱️

登录后开始阅读

当前 MVP 版本只开放邮箱密码登录注册。完成登录后即可进入面试题内容。

还没有账号?先去注册