首页 >> 大全

如何保证数据库和缓存双写一致性

2023-12-23 大全 34 作者:考证青年

数据库缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。

接下来我和大家一起来探讨一下

常见方案

通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们是这样使用缓存的:

简单来说就是先查缓存,如果缓存中没有数据再去查数据库,同时将数据库中查出的结果更新同步至缓存,如果数据库中也没有,则返回空。

这种缓存用法表面上看着很合理,可是我们忽略了一些重要的场景

如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?

不更新缓存行不行?

答案是肯定不行,要不然本文也就没有存在的必要了。

如果长期不更新,只依赖于缓存的过期时间的话,那么用户可能在很长一段时间内使用的都是旧的数据,比如:如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么就会出现上面这个情况。

那么,我们该如何更新缓存呢?

目前有以下4种方案:

我们看看这些方案到底行不行的通

1.先写缓存,再写数据库

我们知道缓存快的主要原因是其 I/O的瓶颈,由于缓存直接读写内存,所以操作速度很快,但是直接读写内存的话,如果遇到缓存数据库宕机,就会导致写入到内存数据丢失,同时我们还会遇到当写入缓存成功之后,如果刚写完缓存,突然网络出现了异常,导致写数据库失败了,这种情况是缓存有,数据库没有,此时缓存中的数据就变成了脏数据。

我们必须要理解缓存的主要目的是暂存,也就是说把数据库的数据临时保存在内存,便于后续的查询,提升查询速度,但如果某条数据,在数据库中都不存在,那这条数据可以说是毫无意义。因此这种方案不可取。

2.先写数据库,再写缓存

这种方案可以避免假数据带来的问题,所谓假数据就是指数据库中没有,但是缓存中有的数据就是假数据。但是新的问题又产生了。

2.1 写缓存失败了

如果把写数据库和写缓存操作,放在同一个事务当中,当写缓存失败了,我们可以把写入数据库的数据进行回滚。

这种场景也可以接受,但是只能应用于并发量较小的业务场景,对接口性能要求不太高的系统,可以这么做。

但如果在高并发的业务场景中,写数据库和写缓存,都属于远程操作。为了防止出现大事务,造成的死锁问题,通常建议写数据库和写缓存不要放在同一个事务中。

如果同时写入缓存和数据库就会出现写数据库成功了,但写缓存失败了,数据库中已写入的数据不会回滚,导致数据库是新数据,而缓存是旧数据,两边数据不一致的情况。

2.2 高并发场景

假设在高并发场景下,针对同一个用户的同一条数据,有两个写数据请求:a和b,它们同时请求到业务系统,其中请求a获取的是旧数据,而请求b获取的是新数据,如下图所示:

我么就按照上图的流程走一遍:

上面的过程就会出现一个很严重的情况:数据库中是新值,而数据库中是旧值。

而且这种方式会严重浪费系统资源。

为什么这么说呢?

如果写的缓存,并不是简单的数据内容,而是要经过非常复杂的计算得出的最终结果。这样每写一次缓存,都需要经过一次非常复杂的计算,不是非常浪费系统资源吗!!!

尤其是当我们遇到的业务场景是写多读少,在这类业务场景下,每一次写操作都需要写一次缓存,这样的话就有点得不偿失了。

3.先删缓存在写数据库

上面两种方案我们可以看到直接更新缓存的问题很多。

怎么保证缓存和数据库双写一致_缓存数据一致性解决方案_

所以我们换一种思路:不去直接更新缓存,而改为删除缓存

但是删缓存也有两种方案:

3.1 先删缓存,在写数据库

大致流程如下:

这个流程有没有问题呢?

我们一起来分析一下

还是先讨论一下高并发下的问题

3.1.1 高并发下的问题

假设在高并发的场景中,同一个用户的同一条数据,有一个读数据请求c,还有另一个写数据请求d(一个更新操作),同时请求到业务系统。如下图所示:

上图流程如下:

在这个过程当中,请求d的新值并没有被请求c写入缓存,同样会导致缓存和数据库的数据不一致的情况。

那么,这种场景的数据不一致问题,能否解决呢?

3.1.2 缓存双删

针对上面这种场景有一种很简单的处理办法,思路很简单:

当d请求写入成功之后,我们在将缓存重删一次。

这就是我们所说的缓存双删,即:

即在写数据库之前删除一次,写完数据库后,再删除一次。

该方案有个非常关键的地方是:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后。

有了缓存删除方案之后,我们在回顾一下高并发下的场景问题:

这样看确实解决了缓存不一致的问题,但是为什么我们非得等一会在删除缓存呢?

请求d卡顿结束,把新值写入数据库后,请求c将数据库中的旧值,更新到缓存中。

此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。我们必须要搞清楚,我们之所以要再删除一次缓存的原因是因为c请求导致缓存中更新了数据库中旧值,我们需要把这个旧值删除掉,所以必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了,删除删除太快,可能后面。

现在解决了一个问题之后,又遇到一个问题:如果第二次删除缓存时,删除失败了该怎么办呢?

由于下面的场景同样也会遇到这个问题,所以我单独拿出来讲解缓存删除失败的解决方案。

3.2 先写数据库,再删缓存

从前面得知,先删缓存,再写数据库,在并发的情况下,也可能会出现缓存和数据库的数据不一致的情况。

接下来,我们重点看看先写数据库,再删缓存的方案。

在高并发的场景中,有一个读数据请求,有一个写数据请求,更新过程如下:

在这个过程中,只有请求f读了一次旧数据,后来旧数据被请求e及时删除了,看起来问题不大,但如果是读数据请求先过来呢?

这种情况有问题吗?

完全没问题!!!

但是呢,我们别忘了,我们的缓存如果设置了有效期,即缓存 自己失效了。

上面的流程大致如下:

当然这种情况发生的概率比较下,只有同时满足:

查询数据库的速度,一般比写数据库要快,更何况写完数据库,还要删除缓存。所以绝大多数情况下,写数据请求比读数据情况耗时更长。

我们先做个总结:

推荐大家使用先写数据库,再删缓存的方案,虽说不能100%避免数据不一致问题,但出现该问题的概率,相对于其他方案来说是最小的。

但是这个方案也会也到缓存删除失败的情况,解决缓存删除失败的情况

4.删缓存失败怎么办?

如果缓存删除失败了,也会导致缓存和数据库的数据不一致。

所以为了解决这个方案,我们加入一个重试机制。

在接口中如果更新了数据库成功了,但更新缓存失败了,可以立刻重试3次。如果其中有任何一次成功,则直接返回成功。如果3次都失败了,则写入数据库,准备后续再处理。

当然,如果你在接口中直接同步重试,该接口并发量比较高的时候,可能有点影响接口性能,这个我们不怕,可以改为异步。

异步重试方式有很多种:

4.1 定时任务

我们创建一个重试表,表中有个字段记录重试次数,初始值为0,同时设置一个最大的重试次数,用一个定时任务异步的去读取重试表中的数据,然后去执行删除缓存操作,每删除一次,重试次数加1,如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则我们需要在重试表中记录一个失败的状态,等待后续进一步处理。

在高并发场景中,定时任务推荐使用-job。相对于xxl-job等定时任务,它可以分片处理,提升处理速度。同时每片的间隔可以设置成:1,2,3,5,7秒等。

使用定时任务重试的话,有个缺点就是实时性没那么高,对于实时性要求特别高的业务场景,该方案不太适用。但是对于一般场景,还是可以用一用的。

但它有一个很大的优点,即数据是落库的,不会丢数据。

4.2 MQ

在高并发的业务场景中,mq(消息队列)是必不可少的技术之一。它不仅可以异步解耦,还能削峰填谷。对保证系统的稳定性是非常有意义的。

mq的生产者,生产了消息之后,通过指定的topic发送到mq服务器。然后mq的消费者,订阅该topic的消息,读取消息数据之后,做业务逻辑处理。

使用mq重试的具体方案如下:

当用户操作写完数据库,但删除缓存失败了,产生一条mq消息,发送给mq服务器。

mq消费者读取mq消息,重试5次删除缓存。如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则写入死信队列中。

当然在该方案中,删除缓存可以完全走异步。即用户的写操作,在写完数据库之后,不用立刻删除一次缓存。而直接发送mq消息,到mq服务器,然后有mq消费者全权负责删除缓存的任务。

因为mq的实时性还是比较高的,因此改良后的方案也是一种不错的选择。

4.3

前面两种删除的重试方案 都有一定的侵入性:

其实,还有一种更优雅的实现,即监听,比如使用:canal等中间件。

具体方案如下:

这套方案中业务接口确实简化了一些流程,只用关心数据库操作即可,而在订阅者中做缓存删除工作。

但是呢,这种方案还会出现删除失败的情况,因为基于实现的删除只会删除一次,所以我们最终还是需要依赖于基于定时任务或者mq的重试机制。

在订阅者中如果删除缓存失败,则发送一条mq消息到mq服务器,在mq消费者中自动重试5次。如果有任意一次成功,则直接返回成功。如果重试5次后还是失败,则该消息自动被放入死信队列,后面可能需要人工介入。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了