Redis缓存双写一致性

文章目录

  • 1、缓存双写一致性的理解
  • 2、数据库和缓存一致性的几种更新策略
    • 2.1 先更新数据库,再更新缓存
    • 2.2 先更新缓存,再更新数据库
    • 2.3 先删除缓存,再更新数据库
    • 2.4 先更新数据库,再删除缓存

1、缓存双写一致性的理解

Redis缓存双写一致性_第1张图片
如果redis中有数据:需要和数据库中的值相同
如果redis中无数据:数据库中的值要是最新值,且准备回写redis

缓存按照操作来分,可细分为两种:只读缓存和读写缓存

只读缓存很简单:就是Redis只做查询,有就是有,没有就是没有,不会再进一步访问MySQL,不再需要会写机制

大部分都是读写缓存,又分为两种:
同步直写策略
写数据库后也同步写redis缓存,缓存和数据库中的数据一 致
对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
比如特别重要的数据、热点敏感数据,例如充值后就需要立马更新,及时生效

异步缓写策略
正常业务运行中,mysq|数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统、积分变更
异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
Redis缓存双写一致性_第2张图片

业务流程写得没错,对于QPS(每秒查询率)小于1000可以使用,但是对于高并发场景不适于。比如在高并发场景下,许多线程几乎同时(时间间隔得不那么开)查询相同的值,由于redis中没有,会全部直接打到MySQL上,MySQL可能就会被打宕机,即使没宕机,这些线程又会进行大量的回写,而且回写的还是同一个值。这里的本质就是:查询MySQL和回写不是原子操作。

对于上述情况,需要采用双检测加锁策略,类似于单例模式中的懒汉模式(可以采用双检测加锁策略实现)

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。

Redis缓存双写一致性_第3张图片

2、数据库和缓存一致性的几种更新策略

目的:无论怎么操作,我们要达到最终一致性

给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。

可以停机的情况下
挂牌报错,凌晨升级,温馨提示,服务降级
单线程,这样重量级的数据操作最好不要多线程

不可以停机的情况,则有4种更新策略

2.1 先更新数据库,再更新缓存

异常问题1:

  1. 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个
  2. 先更新mysq|修改为99成功,然后更新redis
  3. 此时假设异常出现,更新redis失败了,这导致mysq|里面的库存是99而redis里面的还是100
  4. 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据

例如:正当需要更新redis的数据时,此时可能master主机宕机,salve正在处于上位(成为新的master)的过程,没时间来回应更新redis的数据,或者更新redis的操作丢失,这就会导致mysql更新成功,redis更新失败,后续所有读操作都读到了脏数据

异常问题2:

A、B两个线程同时发起调用(实际上可能会存在更多的线程)
正常逻辑

  1. 线程A更新mysql中的某个值,例如a,更新为100
  2. 线程A更新redis中的a为100
  3. 线程B更新mysql中的a,更新为80
  4. 线程B更新redis中的a为80

异常逻辑
多线程环境下,A、B两个线程有快有慢,有前有后有并行

  1. 线程A更新mysql中的a,更新为100
  2. 线程B更新mysql中的a,更新为80
  3. 线程B更新完mysql,立刻回写更新redis中的a为80
  4. 线程A更新redis中的a为100

最终结果,mysq|和redis数据不一 致

2.2 先更新缓存,再更新数据库

从技术上可以做,但不太推荐,业务上一般把mysq|作为底单数据库,保证最后解释

异常问题

正常逻辑
A、B两个线程同时发起调用(实际上可能会存在更多的线程)

  1. 线程A更新redis中的a为100
  2. 线程A更新mysql中的a为100
  3. 线程B更新redis中的a为80
  4. 线程B更新mysql中的a为80

异常逻辑
多线程环境下,A、B两个线程有快有慢,有前有后有并行

  1. 线程A更新redis中的a为100
  2. 线程B更新redis中的a为80
  3. 线程B更新redis中的a为80
  4. 线程A更新mysql中的a为100

最终结果,mysq|和redis数据不一 致

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

异常问题,先删除缓存,再更新数据库(不进行回写),等到再次查询时,才进行回写操作

  1. A线程先成功删除了redis里面的数据,然后去更新mysq|,此时mysq|正在更新中,还没有结束(比如网路延时,或者还没有commit)
  2. 线程B突然要来读取redis缓存数据,由于redis里面的数据是空的,线程B就需要去mysql当中读取数据,此时数据还是旧值
  3. 线程B从mysql中读取到旧值,由于redis中没有缓存,线程B会进行回写操作,把旧值写回redis(刚刚被A线程删除的旧数据又被写回进redis)
  4. A线程终于将数据更新成功
  5. 后续的所有读操作都是读的脏数据

Redis缓存双写一致性_第4张图片
总结:如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysq|时,从数据库中读取到旧值,还写回redis, 导致A白干了

如何解决上诉异常

采用延时双删策略

  1. 线程A先成功删除redis缓存
  2. 线程A更新数据库(更新可能还没有完成)
  3. 线程A更新成功后,先sleep几秒(这几秒表示其他业务逻辑导致耗时延时)
  4. 线程A再次删除redis缓存
  5. 线程B的读取+回写一定是在线程A第二次删除redis缓存之前

加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做"延迟双删"
这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

线程A删除完成之后,休眠的时间该如何确定呢?

方案1:

在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。

方案2:

新启动一个后台监控程序,比如WatchDog监控程序

这种同步淘汰策略,吞吐量降低了怎么办?

将第二次删除作为异步操作,让一个子线程进行异步删除,这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。

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

异常问题

  1. 线程A更新数据库中的值
  2. 线程A还没有来得及删除缓存的值,此时线程B读取缓存,读取的是缓存旧值
  3. 线程A删除缓存
  4. 其他线程进行redis读取操作,利用回写策略,把缓存更新为最新

虽然会出现缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值的这种情况,但是这个方案是最稳妥的

为了解决上诉问题,可以使用消息中间件,来保证数据的最终一致性

Redis缓存双写一致性_第5张图片

流程:

(1)更新数据库数据
(2) 数据库会将操作信息写入binlog日志当中
(3) 订阅程序提取出所需要的数据以及key
(4) 另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作

1、可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
2、当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
3、如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
4、如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

对于Redis和MySQL(或者其它数据库),不能保证数据实时一致性,也就说无论哪一方进行了修改,另一方都能立刻同步,因为其中还存在许多不确定因素,例如网络延时,因此我们需要保证的是最终一致性

你可能感兴趣的:(Redis,redis,数据库,Redis缓存双写一致性)