redis缓存和数据库一致性的解决方案-延迟双删

缓存和数据库不一致性的含义:在redis缓存没有自动失效和过期的长期时间内,redis中的数据和数据库中的数据一直都是不一样的。

在更新数据库数据的这段时间,redis和数据库数据不一致,是正常情况



首先要理解下面这个流程(这是前提,如果不是这个前提,则不考虑缓存和数据库一致性问题)

不论采用哪种方式解决缓存和数据库一致性问题,我们查询数据,都是下面这个流程

(1)先查缓存,缓存查到了数据,则直接结束

(2)缓存中没有数据,去查询数据库获取数据,然后会把数据写入到缓存中,再结束



在并发情况下,这时,有一个更新数据的线程到了,还有一个查询数据的线程到了

不推荐使用修改缓存的方式,因为删除缓存的性能更高,成本更低。

我们先看前面几种方案

(1)采用方案:先更新数据库,再删缓存

初始:缓存中没有数据 x ,只有数据库中有

查询数据线程先到,到第3步,线程因为其他原因阻塞了

(2)采用方案:先删缓存,再更新数据库

初始:缓存和数据库中都有 x = 1 

更新数据线程先到

以上两种方式,都有可能导致数据不一致性问题。



方案:先删缓存,再更新数据库

导致缓存和数据库数据不一致性问题的主要原因是:

更新数据库线程 删除缓存,被另一个查询数据线程 把旧的数据 重新缓存到了 redis中

我们如果等    查询数据线程 把旧的数据 重新缓存到了 redis中(即上图的第7步)完成后, 再删除缓存,就可以了,不过由于不知道 上图的第7步 多久会完成,所以需要延迟一段时间,这个过程也不会很久。

延迟的目的 ,就是为了等  查询数据线程 把旧的数据 重新缓存到了 redis中  完成。

如果不延迟,就直接二次删除缓存, 可能 第7步还没有开始(即查询数据线程  还没开始 把旧的数据 缓存到了 redis中)。这样我们第二次删除,啥也没删到。双删的动作都做完了,第7步又开始了。

特殊情况:如果 第7步 的开始时间,大于 延迟双删的时间,仍然会造成数据不一致性



采用了延迟双删,只会在延迟删除的这一小段时间里数据不一致性。

那能不能保证这一小段时间里,数据也是一致的呢?那就是数据的强一致性,这是CP。

如果要保证强一致性,就要保证redis和数据库的这两步操作是原子性的,这就需要加一把锁。而加锁就会降低系统的吞吐量。

思考我们使用redis是为了什么?使用redis就是为了提高系统的吞吐量的,所以加锁来满足CP强一致性是不可取的。

强一致性(CP)和高性能(高可用性AP)两者只能保证一种

所以延迟双删是可以保证最终的一致性的,即AP。

我们不会采用强一致性,因为会影响系统的吞吐量。

延迟双删的延迟时间如何设定的呢?一般是设置几百毫秒

比如说延迟500ms,那在这500ms以内,那redis中就一直是脏数据,这个没有办法,我们只能保证最终的数据一致性

如何设置一个合理的值?我们需要自己去评估查询数据的线程的耗时,即从数据库读取数据,再写入缓存,这个时间为多少。

延迟的时间大于这个值即可,防止查询数据的线程覆盖掉新的数据



这里在先删缓存,再修改数据库,同时延迟双删的情况下,还是有可能删除缓存失败。

(注意:这里的删除缓存失败,是指删除了一个原本就不存在的key,即读取数据的线程太慢了,延迟双删,删除了一个空key。而不是指redis出现故障的那种删除失败)

如果删除失败了,就异步发送需要删除的key到MQ中,启动消费者线程去再次删除。

一旦消费者线程监听到redis中某个key删除失败的消息,那么就执行重试删除。

但是这样线程1还需要发送一条删除失败的消息到mq中,虽然消费者线程的代码不在这里,但是删除失败的消息还是只能线程1去发送。

这样修改数据库数据的线程1,代码就和mq的代码耦合在一起了。


如果要实现解耦的话?怎么操作呢?

可以使用另一个组件canal,通过它可以去监听mysql的binlog日志。当mysql中有数据变动时,canal(可以理解成canal是mysql的从节点)能够立刻感知到,然后mysql的从节点canal会立即通知canal的客户端

前面是因为线程1去执行的延迟双删操作,所以只有线程1知道延迟双删是否成功,所以需要线程1去发送延迟双删失败的消息给mq,这才造成了线程1代码耦合。

我们使用canal去监听mysql的binlog日志,就可以让canal客户端去执行延迟双删的操作,如果canal删除失败了,由canal去发送需要删除的key到MQ中,并由MQ的消费者去执行删除重试的操作。这样就避免了需要线程1去发送延迟双删失败的消息给mq了。

把延迟双删(第二次删除redis)的操作的交给canal客户端,并把删除失败时,异步发送需要删除的key到MQ中中这个任务交给canal(这个canal客户端可以是一个springboot应用)。(第一次删除redis仍然由修改数据线程,即线程1执行 --> 先删缓存,再修改数据库。)

整个流程如下:

所以更推荐使用先修改数据库,再删缓存的方式,来保持数据库和缓存的数据一致性。不用计较延迟双删的延迟时间。即便在 先修改数据库,再删缓存的方式中,删除了空值,也会由canal(只要数据库中的值修改了,就去删除redis)进行二次删除,如果canal失败了,也会由MQ的消息订阅者去重试删除。

  • 11
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

#学习的路上

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

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

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

打赏作者

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

抵扣说明:

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

余额充值