首页 > 数据库 >如何保障MySQL和Redis的数据一致性?

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

时间:2023-10-21 11:36:31浏览次数:34  
标签:缓存 请求 删除 Redis 先写 MySQL 一致性

先抛一下结论:在满足实时性的条件下,不存在两者完全保存一致的方案,只有最终一致性方案。根据网上的众多解决方案,总结出 6 种,直接看目录:

 

图片

 

不好的方案

 

1、先写 MySQL,再写 Redis

 

图片

 

 

如图所示:

 

  • 这是一副时序图,描述请求的先后调用顺序;

 

  • 橘黄色的线是请求 A,黑色的线是请求 B;

 

  • 橘黄色的文字,是 MySQL 和 Redis 最终不一致的数据;

 

  • 数据是从 10 更新为 11;

 

  • 后面所有的图,都是这个含义,不再赘述。

 

请求 A、B 都是先写 MySQL,然后再写 Redis,在高并发情况下,如果请求 A 在写 Redis 时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。

 

这个图已经画得很清晰了,我就不用再去啰嗦了吧,不过这里有个前提,就是对于读请求,先去读 Redis,如果没有,再去读 DB,但是读请求不会再回写 Redis。大白话说一下,就是读请求不会更新 Redis。

 

2、先写 Redis,再写 MySQL

 

图片

 

 

同“先写 MySQL,再写 Redis”,看图可秒懂。

 

3、先删除 Redis,再写 MySQL

 

这幅图和上面有些不一样,前面的请求 A 和 B 都是更新请求,这里的请求 A 是更新请求,但是请求 B 是读请求,且请求 B 的读请求会回写 Redis。

 

图片

 

 

请求 A 先删除缓存,可能因为卡顿,数据一直没有更新到 MySQL,导致两者数据不一致。

 

这种情况出现的概率比较大,因为请求 A 更新 MySQL 可能耗时会比较长,而请求 B 的前两步都是查询,会非常快。

 

好的方案

 

1、先删除 Redis,再写 MySQL,再删除 Redis

 

对于“先删除 Redis,再写 MySQL”,如果要解决最后的不一致问题,其实再对 Redis 重新删除即可,这个也是大家常说的“缓存双删”。

 

图片

 

 

为了便于大家看图,对于蓝色的文字,“删除缓存 10”必须在“回写缓存10”后面,那如何才能保证一定是在后面呢?网上给出的第一个方案是,让请求 A 的最后一次删除,等待 500ms。

 

对于这种方案,看看就行,反正我是不会用,太 Low 了,风险也不可控。

 

那有没有更好的方案?我建议异步串行化删除,即删除请求入队列。

 

图片

 

 

异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。

 

如果双删失败怎么办,网上有给 Redis 加一个缓存过期时间的方案,这个不敢苟同。个人建议整个重试机制,可以借助消息队列的重试机制,也可以自己整个表,记录重试次数,方法很多。

 

简单小结一下:

 

  • “缓存双删”不要用无脑的 sleep 500 ms;

 

  • 通过消息队列的异步&串行,实现最后一次缓存删除;

 

  • 缓存删除失败,增加重试机制。

 

2、先写 MySQL,再删除 Redis

 

图片

 

 

对于上面这种情况,对于第一次查询,请求 B 查询的数据是 10,但是 MySQL 的数据是 11,只存在这一次不一致的情况,对于不是强一致性要求的业务,可以容忍。(什么情况下不能容忍呢,比如秒杀业务、库存服务等。)

 

当请求 B 进行第二次查询时,因为没有命中 Redis,会重新查一次 DB,然后再回写到 Reids。

 

图片

 

 

这里需要满足 2 个条件:

 

  • 缓存刚好自动失效;

  • 请求 B 从数据库查出 10,回写缓存的耗时,比请求 A 写数据库,并且删除缓存的还长。

 

对于第二个条件,我们都知道更新 DB 肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件的情况更小。

 

3、先写 MySQL,通过 Binlog,异步更新 Redis

 

这种方案,主要是监听 MySQL 的 Binlog,然后通过异步的方式,将数据更新到 Redis,这种方案有个前提,查询的请求,不会回写 Redis。

 

图片

 

 

这个方案,会保证 MySQL 和 Redis 的最终一致性,但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查 DB;如果缓存有数据,查询的数据也会存在不一致的情况。

 

所以这个方案,是实现最终一致性的终极解决方案,但是不能保证实时性。

 

几种方案比较

 

我们对比上面讨论的 6 种方案:

 

1、先写 Redis,再写 MySQL

 

这种方案,我肯定不会用,万一 DB 挂了,你把数据写到缓存,DB 无数据,这个是灾难性的。

 

我之前也见同学这么用过,如果写 DB 失败,对 Redis 进行逆操作,那如果逆操作失败呢,是不是还要搞个重试?

 

2、先写 MySQL,再写 Redis

 

对于并发量、一致性要求不高的项目,很多就是这么用的,我之前也经常这么搞,但是不建议这么做。

 

当 Redis 瞬间不可用的情况,需要报警出来,然后线下处理。

 

3、先删除 Redis,再写 MySQL

 

这种方式,我还没使用过,直接忽略吧。

 

4、先删除 Redis,再写 MySQL,再删除 Redis

 

这种方式虽然可行,但是感觉好复杂,还要搞个消息队列去异步删除 Redis。

 

5、先写 MySQL,再删除 Redis

 

比较推荐这种方式,删除 Redis 如果失败,可以再多重试几次,否则报警出来;这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。

 

6、先写 MySQL,通过 Binlog,异步更新 Redis

 

对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级。

 

纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。

 

结论

 

  • 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。

 

  • 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。

 

作者丨楼仔

标签:缓存,请求,删除,Redis,先写,MySQL,一致性
From: https://www.cnblogs.com/88223100/p/How-to-ensure-data-consistency-between-MySQL-and-Redis.

相关文章

  • MySQL CASE()用法
    MySQL中的CASE表达式用于根据条件进行条件判断和返回多个可能的值。它允许在查询中执行简单的逻辑判断,并根据不同的条件返回不同的值或执行不同的操作。CASE表达式有两种形式:简单CASE和搜索CASE。简单CASE形式:CASEexpressionWHENvalue1THENresult1WHE......
  • MySQL IF()用法
    MySQL中的IF函数用于根据条件返回两个值中的一个。它可以应用于复杂查询和更新语句中,以便根据条件动态生成值。IF函数的语法如下:IF(condition,value_if_true,value_if_false)其中,condition是一个布尔表达式或一个返回布尔值的函数,value_if_true是条件为TRUE时返回......
  • Mysql FIND_IN_SET()用法
    MySQL中的FIND_IN_SET函数用于在逗号分隔的字符串列表中查找指定字符串的位置。它接受两个参数:要查找的字符串和逗号分隔的字符串列表。语法如下:FIND_IN_SET(string,string_list)其中,string是要查找的字符串,string_list是逗号分隔的字符串列表。返回值为待查找字符串......
  • MySql 关键字 COALESCE
    MySQLCOALESCE()函数用于返回表达式列表中的第一个非空值。如果列表中的所有值均评估为NULL,则COALESCE()函数将返回NULL。COALESCE()函数接受一个参数,该参数是可以包含各种值的列表。MySQLCOALESCE()函数返回的值是表达式列表中的第一个非空值;如果列表中的所有值均为NULL,则为N......
  • MySql Json字段部分查询语法
    模糊匹配jsonObject字段select*fromtableNamewherecolumnName->'$.xx'like'%xx%'精确匹配jsonObject类型字段select*fromtableNamewherecolumnName->'$.xx'='xx'模糊匹配jsonArray字段select*fromtableNamewh......
  • laravel:访问redis(10.27.0)
    一,相关文档:https://learnku.com/docs/laravel/10.x/redis/14887二,php代码1,配置.env使用默认的设置:REDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=63792,controller中引用:12345678910111213141516171819202122232425<?ph......
  • celery包结构、celery延迟任务和定时任务、django中使用celery、接口缓存、双写一致性
    celery包结构project├──celery_task#celery包│├──__init__.py#包文件│├──celery.py#celery连接和配置相关文件,且名字必须叫celery.py│└──tasks.py#所有任务函数├──add_task.py#添加任务......
  • MySQL学习(6)单表访问方法
    什么是访问方法MySQL执行查询语句的方式称为访问方法(accessmethod)。单表的访问方法有哪些const通过主键或唯一二级索引列定位一条记录的方法叫const,这种方法要求主键列或者唯一二级索引列与一个常数进行等值比较时才有效。注意,使用唯一二级索引且搜索条件为ISNULL无效。例......
  • redis其他操作、redis管道、django中使用redis、django缓存、celery介绍、补充单例
    redis其他操作'''delete(*names)exists(name)keys(pattern='*')expire(name,time)rename(src,dst)move(name,db))randomkey()type(name)'''#redis的key值,最大可以是多少?最大不超过512M一般1KB#redis的value值,最大可以是多少?最大不超过512M......
  • Nacos一致性协议
    一、概要Nacos是阿里开放的一款中间件,它主要提供三种功能:持久化节点注册,非持久化节点注册和配置管理。二、一致性协议-AP/CPNacos不是纯粹的AP服务,也不是纯粹的CP服务,而是两者同时支持。这要从服务注册说起,Provider启动时将自身的信息注册至注册中心,如果注册中心是Zookeeper......