Redis缓存双写一致性

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

在这里插入图片描述
如果redis中有数据需要和数据库中的值相同
如果redis中无数据数据库中的值要是最新值且准备回写redis

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

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

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

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

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

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

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

在这里插入图片描述

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. 后续的所有读操作都是读的脏数据

在这里插入图片描述
总结如果数据库更新失败或超时或返回不及时导致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时缓存命中读取到的是缓存旧值的这种情况但是这个方案是最稳妥的

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

在这里插入图片描述

流程

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

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

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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: redis