6786 字
约 22 分钟
2
MySQL + Redis 如何保证数据一致性?6 种方案一次讲透

MySQL + Redis 如何保证数据一致性?6 种方案一次讲透

在实际项目中,我们经常会使用:

MySQL + Redis

这样的组合。

MySQL 负责持久化数据,Redis 负责扛住大量查询请求。

最典型的查询流程就是:

请求
  ↓
查询 Redis
  ↓
命中?
 /   \
是    否
|     |
返回  查询 MySQL
        ↓
      回写 Redis
        ↓
       返回

看起来非常简单。

但只要系统同时存在读请求和更新请求,一个经典问题马上就来了:

MySQL 和 Redis 的数据不一致怎么办?

比如商品价格原来是:

10

现在后台把价格修改成:

11

理想状态当然是:

MySQL = 11
Redis = 11

但问题在于:

更新 MySQL 和更新 Redis 是两个独立操作。

于是就可能出现:

MySQL = 11
Redis = 10

甚至:

MySQL = 10
Redis = 11

如果再把高并发考虑进来,事情就更麻烦了。

这篇文章,我们就把 MySQL + Redis 常见的 6 种更新方案一次讲清楚:

1. 先写 MySQL,再写 Redis
2. 先写 Redis,再写 MySQL
3. 先删除 Redis,再写 MySQL
4. 先删除 Redis,再写 MySQL,再删除 Redis
5. 先写 MySQL,再删除 Redis
6. 先写 MySQL,通过 Binlog 异步更新 Redis

最后再回答一个真正重要的问题:

生产环境到底应该选哪一种?


一、先搞明白:为什么 MySQL 和 Redis 会不一致?

很多人第一次接触这个问题,会想:

不就是更新两份数据吗?都更新一下不就行了?

问题恰恰就出在:

更新两份数据

这件事情上。

假设我们的系统里有:

MySQL
Redis

现在需要把商品价格:

10 → 11

那么至少要执行两个操作:

UPDATE MySQL

更新/删除 Redis

关键问题来了:

这两个操作没办法天然组成一个原子操作。

也就是说,不可能简单保证:

要么两个都成功
要么两个都失败

可能出现:

MySQL 成功
Redis 失败

也可能:

Redis 成功
MySQL 失败

甚至两个操作都成功了,在高并发情况下,因为执行顺序发生交错,最终结果依然可能是错的。

所以 MySQL + Redis 一致性问题,本质上其实是在解决:

两个独立存储系统之间的数据同步问题。


二、方案一:先写 MySQL,再写 Redis

这是最容易想到的方案。

更新数据时:

更新 MySQL
    ↓
更新 Redis

比如原来的数据是:

MySQL = 10
Redis = 10

请求 A 要把数据修改成 11:

请求 A

MySQL:10 → 11
Redis:10 → 11

正常情况下当然没有问题。

但是一旦出现并发,就不一定了。

假设现在有两个请求:

请求 A:10 → 11

请求 B:11 → 12

正常顺序应该是:

A 更新 MySQL = 11
A 更新 Redis = 11

B 更新 MySQL = 12
B 更新 Redis = 12

最终:

MySQL = 12
Redis = 12

没有问题。

但如果请求 A 更新完 MySQL 之后突然卡住:

时间 ↓

请求 A                请求 B

MySQL = 11
   |
   | 卡住
   |
                      MySQL = 12
                      Redis = 12
   |
   | 恢复
   ↓
Redis = 11

最后就变成:

MySQL = 12
Redis = 11

出问题了。

这里最关键的一点是:

数据库的更新顺序,不一定等于 Redis 的更新顺序。

两个并发请求发生交错之后,旧请求反而可能最后覆盖 Redis。

所以:

先写 MySQL
再写 Redis

虽然简单,但是在并发环境下存在明显的数据覆盖问题。

因此一般不推荐把 Redis 当成需要和数据库同步更新的第二份数据源。


三、方案二:先写 Redis,再写 MySQL

那反过来呢?

更新 Redis
    ↓
更新 MySQL

看起来似乎也可以。

例如:

Redis:10 → 11
MySQL:10 → 11

但这个方案的问题其实更加明显。

假设:

Redis 更新成功

变成:

Redis = 11

然后更新 MySQL 时:

MySQL 挂了

或者:

SQL 执行失败
事务回滚
数据库连接异常

那么最后:

Redis = 11
MySQL = 10

这就非常危险。

因为在绝大多数系统里:

MySQL 才是真正的数据源,Redis 只是缓存。

现在缓存里面出现了一份数据库根本不存在的数据,相当于:

缓存自己创造了一份数据

这显然不合理。

有人可能会说:

那 MySQL 更新失败,我再把 Redis 改回去不就好了?

于是流程变成:

更新 Redis
↓
更新 MySQL
↓
失败
↓
回滚 Redis

新的问题马上又来了:

Redis 回滚也失败怎么办?

再重试?

重试也失败怎么办?

继续补偿?

系统复杂度一下就上来了。

所以:

先 Redis,后 MySQL,一般是最不推荐的方案之一。

核心原因非常简单:

不要让缓存跑到真正的数据源前面。


四、为什么很多系统选择“删除缓存”,而不是“更新缓存”?

讲第三种方案之前,我们先解决一个非常重要的问题。

前面两种方案都是:

更新 MySQL
+
更新 Redis

但真实项目中更常见的做法其实不是:

更新 Redis

而是:

删除 Redis

为什么?

假设商品信息在 MySQL 里是:

商品ID:1001
价格:100
库存:200
名称:iPhone
状态:上架

Redis 中可能缓存的是多个字段经过业务计算之后的结果。

每次数据库发生变化,都主动重新计算 Redis:

更新 DB
↓
重新组装缓存
↓
更新 Redis

会让更新链路变复杂。

另一种思路就简单很多:

更新 DB
↓
删除 Redis

下一次有人查询:

查询 Redis
↓
没有
↓
查询 MySQL
↓
重新写 Redis

这就是非常经典的:

Cache Aside Pattern

也就是旁路缓存模式。

它的核心思想是:

数据库负责保存真实数据,缓存只是数据库数据的副本。

缓存不对了怎么办?

最简单的方式不是努力把它改正确,而是:

直接删掉

下一次查询的时候重新从数据库构建。

这也是理解后面几种方案的关键。


五、方案三:先删除 Redis,再写 MySQL

既然更新缓存容易出现问题,那我们删除缓存。

流程变成:

删除 Redis
    ↓
更新 MySQL

乍一看很合理。

比如:

Redis = 10
MySQL = 10

请求 A 修改数据:

删除 Redis
↓
MySQL:10 → 11

更新结束以后:

Redis = null
MySQL = 11

下一次查询:

查询 Redis
↓
缓存未命中
↓
查询 MySQL = 11
↓
回写 Redis = 11

最终:

MySQL = 11
Redis = 11

非常完美。

但是并发一来,又出问题了。

假设:

请求 A:更新请求
请求 B:查询请求

发生下面这种情况:

时间 ↓

请求 A                     请求 B

删除 Redis
   |
   | 卡住
   |
                           查询 Redis
                           ↓
                           没有数据
                           ↓
                           查询 MySQL
                           ↓
                           得到旧数据 10
                           ↓
                           Redis = 10
   |
   | 恢复
   ↓
MySQL = 11

最后:

MySQL = 11
Redis = 10

又不一致了。

问题出现在哪里?

因为:

删除缓存

和:

更新数据库

之间存在一个时间窗口。

在这个窗口里,读请求发现:

缓存没了

于是去查询数据库。

但是数据库此时:

还没有更新

所以读到了旧值。

然后又把旧值写回 Redis。

最终就产生了脏缓存。

因此:

先删除缓存,再更新数据库,也不是一个理想方案。


六、方案四:延迟双删

既然:

删除 Redis
↓
更新 MySQL

会因为中间插入一个查询请求而产生脏数据,那怎么办?

很自然会想到:

那更新完数据库以后,我再删一次缓存。

于是出现了经典的:

延迟双删

流程:

第一次删除 Redis
        ↓
更新 MySQL
        ↓
等待一段时间
        ↓
第二次删除 Redis

为什么第二次删除有用?

还是刚才那个场景:

请求 A:更新
请求 B:查询

可能发生:

A 删除 Redis
↓
B 查询 Redis:没有
↓
B 查询 MySQL:10
↓
A 更新 MySQL:11
↓
B 回写 Redis:10
↓
A 再删除 Redis

最后:

MySQL = 11
Redis = null

下一次查询:

Redis miss
↓
MySQL = 11
↓
Redis = 11

问题解决。

所以第二次删除的目的其实非常明确:

清理更新数据库期间,其他请求可能重新写入的旧缓存。


七、为什么“sleep 500ms 再删除”并不优雅?

网上讲延迟双删的时候,经常可以看到这样的方案:

删除 Redis
↓
更新 MySQL
↓
sleep 500ms
↓
删除 Redis

核心思想是:

等一会,让之前可能正在执行的读请求完成缓存回写,然后我再删掉它。

逻辑上说得通。

问题是:

为什么是 500ms?

如果查询耗时:

100ms

500ms 足够。

如果碰到:

慢 SQL
网络抖动
GC
线程调度
数据库负载升高

导致查询耗时:

800ms

那 500ms 就不够。

如果为了安全改成:

2 秒

又意味着业务线程白白等待 2 秒。

所以这种:

Thread.sleep(500);

最大的问题就是:

这个时间很难精确评估,而且直接阻塞业务线程。

因此更工程化的方式应该是:

更新 MySQL
↓
发送删除缓存消息
↓
MQ
↓
异步消费
↓
删除 Redis

甚至可以让同一个业务 Key:

user:1001

进入同一个有序队列,进行:

串行删除

这样既不会阻塞业务线程,又能够降低并发乱序带来的问题。

如果删除 Redis 失败:

第一次失败
↓
重试
↓
第二次失败
↓
重试
↓
超过阈值
↓
告警 / 补偿

这样就比简单:

sleep 500ms

靠谱很多。


八、方案五:先写 MySQL,再删除 Redis

终于来到实际项目里非常常见,也是比较推荐的一种方案:

更新 MySQL
    ↓
删除 Redis

注意它和第三种方案只是顺序反过来了。

第三种:

删除 Redis
↓
更新 MySQL

现在变成:

更新 MySQL
↓
删除 Redis

为什么这个小小的顺序变化会更好?

我们来分析。

假设:

MySQL = 10
Redis = 10

请求 A 要修改成:

11

正常流程:

MySQL = 11
↓
删除 Redis

最终:

MySQL = 11
Redis = null

请求 B 再查询:

Redis miss
↓
查询 MySQL
↓
得到 11
↓
Redis = 11

没有问题。


九、这种方案就 100% 不会不一致吗?

不是。

这一点非常重要。

很多文章会直接告诉你:

先更新数据库,再删除缓存

然后结束。

但它实际上依然存在极端并发问题。

假设缓存:

刚好过期

然后发生:

请求 B:查询
请求 A:更新

一种极端时序是:

时间 ↓

请求 B                     请求 A

查询 Redis
↓
缓存刚好失效
↓
查询 MySQL
↓
得到旧值 10
                             更新 MySQL = 11
                             ↓
                             删除 Redis
↓
回写 Redis = 10

最终:

MySQL = 11
Redis = 10

还是不一致。

但是注意:

要出现这个问题,需要同时满足几个比较苛刻的条件。

首先:

缓存恰好失效

其次:

读请求查询到了旧数据

而且这个读请求从:

查询数据库

到:

写入 Redis

整个过程还必须比:

更新数据库
+
删除 Redis

更慢。

换句话说,需要形成:

读旧数据
↓
更新 DB
↓
删除缓存
↓
旧数据最后才写进 Redis

这个非常特殊的时序。

现实情况下通常:

数据库查询

比:

数据库更新

更快。

因此虽然理论上存在这种可能,但实际出现概率通常很低。

这就是为什么:

先更新 MySQL,再删除 Redis,是 Cache Aside 模式下非常常用的工程实践。

它不是数学意义上的强一致。

而是:

用相对简单的方案,把不一致出现的概率压到足够低。

这其实就是工程设计非常重要的思想。


十、删除 Redis 失败怎么办?

真正上线之后,还有一个不能忽略的问题:

MySQL 更新成功
↓
Redis 删除失败

这时候:

MySQL = 11
Redis = 10

旧缓存仍然存在。

所以:

更新 DB
↓
删除 Redis

并不代表事情结束了。

还需要考虑:

删除失败重试。

最简单可以:

删除 Redis
↓
失败
↓
重试 1
↓
失败
↓
重试 2
↓
失败
↓
记录日志 + 告警

更完善一些可以使用:

消息队列

例如:

业务请求
   |
   v
更新 MySQL
   |
   v
发送缓存删除消息
   |
   v
Kafka / RocketMQ
   |
   v
消费者删除 Redis
   |
   +----成功----> END
   |
   +----失败----> 重试

这样缓存删除失败之后,可以借助消息系统提供的重试能力继续处理。

当然,引入 MQ 以后系统复杂度也会上升。

所以还是那句话:

一致性做到什么程度,最终取决于业务要求。


十一、方案六:MySQL Binlog + 异步更新 Redis

如果我们的目标不是:

尽可能实时

而是:

最终一定一致

那么还有一种非常经典的方案:

监听 MySQL Binlog。

流程:

业务请求
   |
   v
更新 MySQL
   |
   v
产生 Binlog
   |
   v
Binlog 订阅程序
   |
   v
消息队列
   |
   v
消费者
   |
   v
更新 / 删除 Redis

比如:

MySQL:10 → 11

数据库事务提交以后,会产生对应的 Binlog。

我们可以通过类似:

Canal

这样的组件订阅 Binlog。

然后:

Canal
↓
Kafka
↓
Consumer
↓
Redis

最终把 Redis 更新成:

11

这种方案最大的好处是:

缓存同步从业务代码里解耦出来了。

业务代码只需要:

更新 MySQL

至于:

Redis
Elasticsearch
数据仓库
其他下游系统

都可以消费数据库变更事件进行同步。

整个架构就变成:

                   MySQL
                     |
                   Binlog
                     |
                   Canal
                     |
                   Kafka
                /      |      \
               /       |       \
              v        v        v
           Redis      ES      数据仓库

这其实已经不仅仅是:

缓存一致性

而是:

数据变更事件驱动

了。


十二、Binlog 方案有什么缺点?

没有免费的午餐。

Binlog 方案最大的特点就是:

异步

既然是异步,就意味着:

MySQL 更新成功之后,Redis 不可能在同一个瞬间立即更新。

比如:

T1:MySQL = 11

T2:Binlog 被消费

T3:Redis = 11

那么:

T1 ~ T3

之间就存在一个短暂的不一致窗口。

如果这时候有人查询:

Redis

可能还是:

10

所以这种方案保证的是:

最终一致性

而不是:

强一致性。

它特别适合:

异地数据同步
搜索索引同步
数据仓库
数据汇总
缓存异步刷新
跨系统数据分发

但如果是:

秒杀库存
抢购资格
账户余额

这种对实时正确性要求非常高的数据,就不能简单依赖一个异步缓存同步方案解决核心一致性问题。


十三、6 种方案放在一起比较

现在我们把前面的内容全部放在一起。

方案 推荐程度 主要问题
先 Redis,再 MySQL ❌ 不推荐 DB 失败后缓存产生脏数据
先 MySQL,再 Redis ⚠️ 不推荐 并发更新可能导致旧值覆盖新值
先删 Redis,再 MySQL ❌ 不推荐 更新 DB 期间读请求可能回写旧缓存
删除 Redis → MySQL → 再删 Redis ⚠️ 可用 实现复杂,需要处理延迟和重试
MySQL → 删除 Redis ✅ 推荐 极端情况下仍可能短暂不一致
MySQL → Binlog → Redis ✅ 最终一致性 存在异步延迟

如果只记住其中最重要的两个方案,可以记:

实时性优先:
MySQL → 删除 Redis

最终一致性:
MySQL → Binlog → MQ → Redis

十四、为什么“先写 MySQL,再删除 Redis”是常见推荐方案?

现在重新看这个方案:

更新 MySQL
↓
删除 Redis

它背后其实有三个非常重要的设计思想。

1. MySQL 是唯一可信数据源

始终坚持:

MySQL = Source of Truth

Redis 只是:

缓存副本

因此永远应该优先保证:

MySQL 正确

而不是:

Redis 正确

2. 缓存错了就删,不要费劲修改

不要:

DB 改成什么
Redis 就跟着改成什么

而是:

DB 更新
↓
Redis 删除

让下一次查询重新构建缓存。

这样系统会简单很多。


3. 接受极小概率的短暂不一致

很多系统根本不需要:

100% 强一致

比如:

用户昵称
文章详情
商品介绍
文章点赞数
排行榜
用户主页

晚几十毫秒或者几百毫秒看到新数据,大多数时候没有什么影响。

如果为了理论上的绝对一致,引入:

分布式事务
复杂锁
多阶段提交
同步等待

反而可能导致:

吞吐下降
延迟升高
系统复杂
故障点增加

所以工程设计真正需要考虑的是:

业务能容忍什么程度的不一致?

而不是一看到 Redis + MySQL,就条件反射地追求强一致。


十五、那什么业务不能这么干?

有些数据就不能简单接受:

最终一致

比如:

库存
余额
优惠券数量
秒杀资格
支付状态
账户资产

假设库存:

只剩 1 件

Redis 显示:

库存 = 1

MySQL 实际已经:

库存 = 0

这时候再放一个请求进来,就可能造成:

超卖

所以这类场景里,一个非常重要的原则是:

不要把普通缓存的一致性方案,当成核心交易数据的一致性方案。

Redis 可以参与:

限流
预扣
资格判断
热点保护

但最终的库存扣减、账户余额等核心状态,需要另外设计完整的一致性机制。

这已经属于:

分布式事务
库存模型
幂等
状态机
消息事务
补偿机制

等更大的话题了。


十六、缓存到底应该设置过期时间吗?

这里还有一个很有争议的问题:

既然我们已经通过删除缓存保证一致性,还需要 TTL 吗?

从一致性的角度来看:

TTL

不应该成为主要保障。

因为假设:

MySQL = 11
Redis = 10
TTL = 30分钟

你不能说:

没事,30 分钟以后它自己就好了。

如果业务要求数据尽快一致,那么:

30分钟错误数据

显然不能接受。

所以:

TTL 更适合作为兜底,而不是主要一致性方案。

正常链路应该是:

更新 DB
↓
主动删除缓存
↓
失败重试

TTL 的作用则更像:

即使所有补偿机制都出问题
↓
错误缓存也不会永久存在

因此在实际系统里,我通常会把:

主动删除
+
失败重试
+
合理 TTL

组合使用。

它们解决的是不同层次的问题。


十七、如果让我设计生产环境方案

如果是普通互联网业务,例如:

文章详情
用户资料
商品信息
配置数据
排行榜信息

我会优先考虑:

                 更新请求
                    |
                    v
                更新 MySQL
                    |
                    v
                删除 Redis
                    |
              +-----+-----+
              |           |
            成功          失败
              |           |
              v           v
             END        重试
                          |
                     多次失败
                          |
                          v
                      MQ / 告警

查询仍然走标准 Cache Aside:

                 查询请求
                    |
                    v
                查询 Redis
                    |
              +-----+-----+
              |           |
            命中         未命中
              |           |
              v           v
            返回       查询 MySQL
                          |
                          v
                     回写 Redis
                          |
                          v
                         返回

这套方案:

实现简单
性能不错
实时性高
维护成本低

对于绝大多数普通缓存场景已经足够。


十八、如果数据同步链路比较复杂呢?

假设系统慢慢变大。

一次 MySQL 数据变化,需要同步:

Redis
Elasticsearch
推荐系统
数据仓库
风控系统
统计系统

如果全部写进业务代码:

updateMysql();

updateRedis();

updateElasticSearch();

sendRecommendEvent();

sendRiskEvent();

sendStatisticsEvent();

业务代码会越来越恐怖。

这时候就可以考虑:

MySQL
  |
Binlog
  |
Canal
  |
Kafka
  |
  +------------+------------+------------+
  |            |            |            |
Redis          ES         风控系统      数据仓库

这也是 Binlog + MQ 真正有价值的地方:

它解决的不只是 Redis 缓存,而是数据库变更如何可靠地分发给多个下游系统。


十九、从这个问题还能学到什么?

MySQL + Redis 一致性,其实是一个非常经典的系统设计问题。

它表面上问的是:

先更新谁?
后更新谁?

但真正往下挖,会发现它涉及很多后端核心知识:

缓存模式
并发
原子性
最终一致性
消息队列
Binlog
失败重试
幂等
补偿机制
分布式系统

其中最值得记住的不是某一个固定答案,而是下面几个原则。

第一:数据库通常才是真正的数据源

MySQL = Source of Truth
Redis = Cache

不要反过来。

第二:缓存更新优先考虑“失效”,而不是“同步修改”

也就是:

更新 DB
↓
删除 Cache

而不是:

更新 DB
↓
更新 Cache

第三:不存在免费的强一致性

只要数据跨越:

MySQL
+
Redis

两个独立系统,就必须面对失败、并发和网络问题。

一致性越强,通常意味着:

更高复杂度
更高延迟
更低吞吐

第四:先问业务允许多大的不一致

普通文章详情:

短暂不一致
通常可以接受

账户余额:

短暂不一致
可能都不能接受

技术方案必须服务于业务要求。

第五:失败永远要考虑重试和补偿

千万不要只写:

更新 MySQL
↓
删除 Redis

然后认为万事大吉。

真正的生产系统还要继续问:

删除失败怎么办?

重试失败怎么办?

消息重复怎么办?

消息丢失怎么办?

服务重启怎么办?

如何发现长期不一致?

这才是从“会写代码”走向“会设计系统”的关键一步。


二十、最后总结

MySQL + Redis 的一致性问题,可以浓缩成一句话:

不要试图把 MySQL 和 Redis 当成两个同等重要的数据源,而应该明确 MySQL 是真实数据,Redis 是可以被删除和重建的缓存副本。

因此,对于普通高并发缓存场景,比较推荐:

更新 MySQL
↓
删除 Redis

也就是:

Cache Aside

同时配合:

删除失败重试
+
异常告警
+
合理 TTL 兜底

如果系统需要更加完善的最终一致性,可以进一步升级:

MySQL
↓
Binlog
↓
Canal
↓
Kafka
↓
Redis

实现异步的数据同步。

所以最终可以把方案选择简单记成:

普通高并发业务
        ↓
MySQL → 删除 Redis
        ↓
实时性更好


复杂数据同步场景
        ↓
MySQL → Binlog → MQ → Redis
        ↓
最终一致性更好

至于:

先 Redis → MySQL
MySQL → 直接更新 Redis
删除 Redis → MySQL

这些方案为什么不够好,现在再回头看,原因就非常清楚了。

其实缓存一致性真正难的,从来都不是记住:

先删还是后删

而是理解:

为什么会产生并发乱序?

为什么会出现脏数据?

哪个系统才是真正的数据源?

失败之后如何恢复?

业务到底需要强一致还是最终一致?

当你能从这几个角度分析问题的时候,面对的不管是:

MySQL + Redis

还是:

MySQL + Elasticsearch
MySQL + Kafka
Redis + DB
数据库 + 搜索引擎

本质上都可以沿着同一套思路继续分析。

因为你真正掌握的,已经不只是:

“Redis 缓存到底应该怎么更新?”

而是:

“分布式系统中,两份数据到底应该如何保持一致?”

MySQL + Redis 如何保证数据一致性?6 种方案一次讲透
http://www.clxhxhhr.top/posts/427/
作者
clxstart
发布于
2026-09-03
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。