如何保障 MySQL 和 Redis 之间的数据一致性

1. 数据一致性

我们知道,Redis 主要是用来做缓存使用,只要使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题。

一般情况下,我们都是先读缓存数据,缓存数据有,则立即返回结果;如果没有数据,则从数据库读数据,并且把读到的数据同步到缓存里,提供下次读请求返回数据

image-20220110113731468

这样能有效减轻数据库压力,但是如果修改删除数据库中的数据,而内存是无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题,那该如何解决呢?

通常的方案有以下几种:

  1. 先更新缓存,在更新数据库;
  2. 先更新数据库,在更新缓存;
  3. 先删除缓存,在更新数据库;
  4. 先更新数据库,在删除缓存。

2. 先更新缓存,在更新数据库

这个方案我们一般不考虑。原因是更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。

3. 先更新数据库,在更新缓存

这个方案也我们一般不考虑,原因跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。

也就是说对于更新缓存来说,一般都不考虑,主要从下面 2 点考虑:

1、并发问题

如果同时有请求 A 和请求 B 进行更新操作,那么会出现:

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

这就出现请求 A 更新缓存应该比请求 B 更新缓存早才对,但是因为网络等原因,B 却比 A 更早更新了缓存。这就导致了脏数据,因此不考虑。

2、业务场景问题

如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

其次很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

而且是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份,也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?

举个例子:一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100次,但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。

实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。

说到底是选择更新缓存还是淘汰缓存呢,主要取决于更新缓存的复杂度,更新缓存的代价很小,此时我们应该更倾向于更新缓存,以保证更高的缓存命中率,更新缓存的代价很大,此时我们应该更倾向于淘汰缓存。但是淘汰缓存操作简单,并且带来的副作用只是增加了一次cache miss,所以一般作为通用的处理方式。

4. 先删除缓存,在更新数据库

该方案也会出问题,具体出现的原因如下:

  1. 如果有两个请求,请求 A(更新操作) 和请求 B(查询操作);
  2. 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;
  3. 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中。

但是此时请求 A 并没有更新成功,或者事务还未提交,请求 B 去数据库查询得到旧值,那么这时候就会产生数据库和 Redis 数据不一致的问题。

那么如何解决呢?最简单的解决办法就是延时双删的策略,即:

  1. 先淘汰缓存;
  2. 再写数据库;
  3. 休眠 1 秒,再次淘汰缓存。

这种做法可以将 1s 内造成的造数据删除。

那么,这个 1 秒怎么确定的,具体该休眠多久呢?

针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百 ms 即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

5. 先更新数据库,在删除缓存

这种方式,被称为 Cache Aside Pattern

对于读请求

  1. 先读缓存,在读数据库;
  2. 若存在,则返回;
  3. 若不存在,读取数据库,然后取出数据后放入缓存,同时返回响应。

对于写请求

  1. 先更新数据库;
  2. 再删除缓存。

这种情况依然存在并发问题?

假设有两个请求,一个请求 A 做查询操作,一个请求 B 做更新操作,那么会有如下情形产生:

  1. 缓存刚好失效;
  2. 请求 A 查询数据库,得一个旧值;
  3. 请求 B 将新值写入数据库;
  4. 请求 B 删除缓存;
  5. 请求 A 将查到的旧值写入缓存。

但这种情况的概率其实是很低的,如果上述情况发生,则步骤 3 的写数据库操作比步骤 2 的读数据库操作耗时更短,才有可能使得步骤 4 先于步骤 5。

但实际上,数据库的读操作的速度远快于写操作的,因此步骤 3 耗时比步骤 2 更短,这一情形很难出现。

但是,理论上来说还是有存在的可能性,那么对于这种情况怎么处理?通常有两种:

  1. 给缓存设计一个过期时间;
  2. 异步延时删除。

6. 删除策略存在的问题

因此,对于 Redis 缓存一致性来说,通常的做法是删除缓存,那么既然存在删除这一动作,如果在删除阶段出现问题,导致数据并没有被删除,那么此时每次查询都是错误数据。这又怎么解决呢?

通常来说存在以下两种方案:

利用消息队列进行删除的补偿重试

image-20220110171208001

  1. 先对数据库进行更新操作;
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败;
  3. 此时将 Redis 的 key 作为消息体发送到消息队列中;
  4. 系统接收到消息队列发送的消息后;
  5. 再次对 Redis 进行删除操作。

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 MySQL 数据库更新操作后再 Binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 Binlog 日志对缓存进行操作。

对于订阅 Binlog 日志可以通过阿里的开源框架 canal,具体可以看看这篇文章:

基于 canal 框架解决 mysql 与 redis 一致性的问题

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪了个王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值