怎么保证缓存和数据库一致性?

有位同学去面试​字节前端实习岗​,然后面试官问了这个问题:"​**BFF 层有 Redis 缓存,怎么保证缓存和数据库一致性?**​"作为从前端转全栈的同学,咱们就来聊聊这个问题。

为什么需要缓存?又为什么不一致?

普通情况下,请求都直接打到数据库,并发量大时 DB 压力剧增。我们在 DB 前面加一层缓存挡请求,这样 DB 压力就会大幅下降。

但是,缓存和数据库是两套独立的存储,只要有数据更新,就不可能同时更新这两套存储系统,所以就会出现数据不一致的问题。

常见思路

先说实际情况:

实际业务里一般不追求强一致,而是追求最终一致。最常见方案是:读的时候先查 Redis,未命中查 DB 再回填;写的时候先更新 DB,再删除 Redis 缓存。

可能有同学就要问了:为什么是"更新数据库,再删除缓存"?

因为如果先删缓存,再更新数据库,在并发场景下可能出现:

Plain 复制代码
线程A:删除 Redis
线程B:Redis 未命中 → 查询 DB,拿到旧数据
线程A:更新 DB
线程B:把旧数据重新写入 Redis

最终 Redis 里反而又变成了旧数据。

所以通常采用:写请求更新 DB,然后删除 Redis

读请求:先查询 Redis,如果命中直接返回;如果未命中,去查询 DB,然后写入 Redis 并返回结果。

但是这样其实并不能够达到百分百一致!

还有一个经典并发场景:

Plain 复制代码
A:更新 DB = 新值
B:查询 DB = 旧值
A:删除 Redis
B:把旧值写入 Redis

因此工程上还会进一步增强。延迟双删!

延迟双删

我们先来回答一个问题,什么是延迟双删。

比如我们在更新 DB 的时候先删除 Redis,然后等待了几十或者几百毫秒。再次删除 Redis。

这样做的目的就是在 DB 更新的时候,第一次删除了缓存,并发读请求查询到旧数据如果旧数据回填到了 Redis,这样就可以通过第二次来删除 Redis 缓存。

不过需要注意:延迟双删不是严格意义上的强一致方案,只是在降低脏缓存概率。

如果想要保证最终一定一致,我们还可以引入 MQ 或者 ​Binlog CDC​。

数据库提交成功后产生变更事件,由异步消费者删除或更新 Redis;同时配合重试机制和幂等处理,即使第一次操作失败,也能够最终把缓存修正过来。
MQ 和 Binlog CDC 它们本质上都是在解决一个问题:数据库已经变了,怎么可靠地通知 Redis "你也赶紧更新/删除"。

这里我们用一个例子来说明。

假设用户修改昵称,旧昵称是"张三",新昵称是"李四"。

正常的做法是先更新 MySQL,然后删除 Redis。比如,我们先把 MySQL 更新为"李四",然后删除掉 Redis。这样下一次查询时,Redis 未命中,就会去查询 MySQL,查到的是我们更改后的结果,最后再把 MySQL 的真实结果缓存到 Redis 中。这样看是没问题的。

但是,如果我们在代码执行中 MySQL 更新成功,然后更新后服务突然宕机,导致下一步 Redis 删除失败,那么最终的结果就是:MySQL 是"李四",而 Redis 还是旧数据"张三"。这样就会导致缓存和数据库不一致。

MQ 怎么解决缓存不一致?

MQ 就是:​Message Queue,消息队列。​ 可以把它理解成一个​中间的传话筒​。

原来是我们的业务服务到 MySQL 更新新数据,再到 Redis 删除旧数据。

有了 MQ 之后,流程就变成了:业务服务更新 MySQL 新数据,然后发送到 MQ 消息队列,消息队列再通知缓存服务,最后到 Redis 删除旧数据。

比如用户在 BFF 层将昵称改为"李四",就会先去 MySQL 更新昵称为"李四",然后发送消息告诉 MQ:"用户某某某的信息变了"。接着,再经过消费者消费,删除 Redis 中该用户缓存。后续 Redis 查询不到,会在 MySQL 中查询,并将 MySQL 数据回填到 Redis 缓存,这样实现了数据一致。

所以 MQ 的核心不是 Redis。

它解决的是:"我现在没办法直接可靠地通知另一个系统,我先把这个事情放进一个消息队列里。"

这样即使业务服务不能马上操作 Redis,也可以由 MQ 后面的消费者继续处理。

我们再回过头看之前 Redis 失败的例子。如果中间加了 MQ 之后,那就变成由消费者删除 Redis。删除失败之后继续重试,如果还是失败,可以再次重试。所以,MQ 最大的价值之一,就是把数据库变更变成一个可以异步重试的事件。

Binlog CDC

其实聪明的同学已经想到了:如果 MySQL 更新成功但是 MQ 发送失败呢?这还不是 DB 中是新数据,而 Redis 中是旧数据,依旧不能做到百分百可靠。所以此时,我们就需要引入 binlog CDC。

MySQL 有一个东西叫:​Binlog(Binary Log),​ 你可以把它理解成:MySQL 自己记录的一份"数据库变更日志"。

比如你执行:

SQL 复制代码
UPDATE user
SET name = '李四'
WHERE id = 123;

MySQL 内部会记录:"用户 123 的 name 由张三变成李四"。

然后来说 CDC:Change Data Capture,也就是 捕获数据库变化。

MySQL 记录的 binlog 通过 CDC 程序监听,发现用户 123 的 name 改变了,就可以通知下游系统。

所以 Binlog CDC = 监听 MySQL 的 Binlog,把数据库变化捕获出来,再发送给其他系统。

只要 MySQL 的事务提交成功,Binlog 就能记录这次变化,CDC 后面仍然可以把它捕获出来。

总结一下

为了方便大家理解,上面的情况中我们并没有特别强调边界情况,防止干扰大家的思路。如果你已经看懂了上面的所有内容,那么接下来我把这些边界情况逐一分析一下,就可以真正学会为什么上面的策略是我们工程上相对的最优解了。

方案 1:先更新数据库,再更新缓存 (不行)

场景​:并发写,请求一改 100、请求二改 200。

  1. 请求一更新库 → 100(还没更新缓存)
  2. 请求二更新库 → 200,紧接着更新缓存 → 200
  3. 请求一才更新缓存 → 100

结果 ​:缓存 = 100,数据库 = 200,​不一致 ​。​根因​​:两个写请求对"库"和"缓存"的更新顺序交错,后更新缓存的请求一反而把值写回了旧的 100。

方案 2:先更新缓存,再更新数据库 (不行)

场景​:同样并发写,请求一改 100、请求二改 200。

  1. 请求一更新缓存 → 100,紧接着请求二更新缓存 → 200
  2. 请求二更新库 → 200,请求一才更新库 → 100

结果 ​:缓存 = 200,数据库 = 100,​不一致 ​。​结论 ​:"更新这种搞不定。" 两种"更新缓存"的顺序都不行,直接排除。

方案 3:先删除缓存,再更新数据库 (不行)

场景​:读写并发,缓存与库初始值都是 100。

  1. 请求二(写)先删除缓存(100 被删)
  2. 请求一(读)读缓存未命中 → 读库拿到 100
  3. 请求二更新库 → 200(写完成)
  4. 请求一把读到的 100 回填进缓存

结果 ​:缓存 = 100,数据库 = 200,​不一致 ​。​根因​​:删缓存后、写库完成前,读请求把"旧 DB 值"回填进了缓存。

方案 4:先更新数据库,再删除缓存 (主方案)

场景 ​:读写并发,且缓存刚好过期不存在,库值 100。

  1. 请求一(读)读缓存未命中 → 读库拿到 100
  2. 请求二(写)更新库 → 200,并删除缓存(写完成)
  3. 请求一把读到的 100 回填进缓存

表面看也不一致 ​,但关键点:​要同时满⾜两个条件才会导致不一致​:

  • 条件 ①:缓存中的数据刚好过期不存在;
  • 条件 ②:修改数据库 的执行时间,比读取数据库的执行时间短(即写比读快)。

为什么概率极低 ​:修改库要加锁,而读可以用 MVCC 实现、不需要锁,所以​绝大多数情况下修改时间比读时间长 ​,条件 ② 很难成立。​结论​:先更新库再删除缓存这种操作发生不一致的概率是比较低的,所以可以采用这种方式。

MVCC 是一种数据库并发控制机制,它通过维护数据的多个历史版本,让读操作和写操作可以同时进行,不需要相互等待,可以大幅提高数据库的并发性能。

它核心解决的问题是:

  1. 避免读写冲突:防止查询操作被正在修改数据的事务阻塞。
  2. 事务隔离:支持不同事务隔离级别(如可重复读、可提交读),保证数据读取的一致性。

方案 5:延迟双删(方案 4 的增强)(最终推荐)

​延迟双删:​ 在"先更新库再删缓存"基础上,​当读请求把旧值回填缓存后,让写请求再删一次缓存​,把回填的旧值清掉。

​实现策略:用 MQ 实现延迟删除,​写请求更新库后,投递一条"删除缓存"消息到 MQ,延迟消费。

  • 延迟时间 :根据读操作的时间来设。

删除消息原本由写请求主动投递 到 MQ;可借助 Canal 自动化(见方案 6)。

方案 6:Canal + binlog 自动投递 MQ(方案 5 的工程化升级)

Canal 利用数据库的​主从同步机制,模拟成数据库的从节点。​ 数据库有数据变化时,通过 binlog 发给 Canal;Canal 接收后​自动把消息投递到 MQ ​;消费者收到消息后执行​删除缓存​。

好处:写请求不必自己主动投递删除消息,业务无侵入、顺序可靠。

面试回答

面试官问:

"Redis 和数据库不一致怎么解决?"

你可以回答:

我一般采用 Cache Aside 模式,写请求先更新数据库,成功后删除 Redis。只有在"缓存刚好过期 + 写库比读库还快"这两个苛刻条件同时满足时才会不一致,又因为改库要加锁、读可用 MVCC 无锁,写通常比读慢,

相关推荐
小月土星1 小时前
吞噬星空 RAG 项目 MySQL 实战:从硬编码登录到数据库鉴权(Day 1)
后端
延凡科技1 小时前
从 “人管机器“ 到 “数据管人“:智慧矿山综合管控落地实践
java·后端·struts
ServBay1 小时前
2026 年值得关注的 8 款 AI 智能体工具
后端·aigc·ai编程
摇曳的精灵2 小时前
前端渲染优化
前端·前端渲染优化
QX_hao2 小时前
【Go】--Cobra-cli的用法
开发语言·后端·golang
大厂码农老A2 小时前
177K star,扒开DeepSeek Harness的营销,我看到了什么
人工智能·后端·deepseek
别看我只是一直狼2 小时前
补充:证书自动续约检查、故障诊断与重新签发
后端
WILLF2 小时前
前端视角:Python requests vs JS axios
前端·python
Lear2 小时前
Java 实现多步骤异步任务编排
后端