Redis篇-详解双写一致

1.什么是双写一致?

话不多说,直接百度:

双写一致就是:确保数据库和缓存中的数据是一致的!


2.为什么会有双写一致问题?

我们都知道,使用缓存的主要目的是为了提升查询的性能。

一般我们是这样使用缓存的:(ps:图出自知乎作者“上弦月”)

  1. 用户请求过来之后,先查缓存有没有数据,如果有则直接返回。
  2. 如果缓存没数据,再继续查数据库。
  3. 如果数据库有数据,则将查询出来的数据,放入缓存中,然后返回该数据。
  4. 如果数据库也没数据,则直接返回空。

 这样看上去很正常,似乎没有什么毛病。

但是,如果数据库的某条数据刚放入缓存,又立马被更新了 ,我们又该如何更新缓存呢??


3.常见方案

我们有以下四种方案:

  1. 先写缓存,再写数据库
  2. 先写数据库,再写缓存
  3. 先删缓存,再写数据库
  4. 先写数据库,再删缓存

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

我们先说先写缓存,再写数据库的情况。

很多人可能首先想到的就是直接更新缓存的情况,

但是如果先写缓存的话,写完缓存之后,在上传数据库的时候,

如果网络出现了问题,数据库没有存上最新的数据,缓存中的数据就变成了的“脏数据”

                           


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

既然上面的方案行不通,那我们试试先写数据库?

我们先写数据库,然后再写缓存,避免了之前的“假数据”,

但是它也带来了新的问题:

把写数据库和写缓存操作,放在同一个事务当中,

当写缓存失败了,我们可以把写入数据库的数据进行回滚,如果是并发量比较小,对接口性能要求不太高的系统,可以这么做。

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

也就是说在该方案中,如果写数据库成功了,但写缓存失败了,数据库中已写入的数据不会回滚。

这就会出现:数据库是新数据,而缓存是旧数据,两边数据不一致的情况。

  1. 请求a先过来,刚写完了数据库。但由于网络原因,卡顿了一下,还没来得及写缓存。
  2. 这时候请求b过来了,先写了数据库。
  3. 接下来,请求b顺利写了缓存。
  4. 此时,请求a卡顿结束,也写了缓存。

很显然,在这个过程当中,请求b在缓存中的新数据,被请求a的旧数据覆盖了。

也就是说:在高并发场景中,如果多个线程同时执行先写数据库,再写缓存的操作,可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况。

该方案还有一个比较大的问题就是:

每个写操作,写完数据库,会马上写缓存,比较浪费系统资源


 3.先删缓存,再写数据库

在用户的写操作中,先执行删除缓存操作,再去写数据库。

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

  1. 请求d先过来,把缓存删除了。但由于网络原因,卡顿了一下,还没来得及写数据库。
  2. 这时请求c过来了,先查缓存发现没数据,再查数据库,有数据,但是旧值。
  3. 请求c将数据库中的旧值,更新到缓存中。
  4. 此时,请求d卡顿结束,把新值写入数据库。

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

1.延迟双删

是一种比较常用的解决方案。

它的基本思路是:先删除缓存,然后更新数据库,最后再设置一个短暂的延时,等这个延时过去之后再删除一次缓存。这样可以确保在更新数据库和删除缓存之间的时间窗口内,即使有新的请求来查询数据,也会因为缓存中没有数据而去数据库中查询,从而保证了数据的一致性。

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

我们再重新回顾一下,高并发下一个读数据请求,一个写数据请求导致数据不一致的产生过程:

  1. 请求d先过来,把缓存删除了。但由于网络原因,卡顿了一下,还没来得及写数据库。
  2. 这时请求c过来了,先查缓存发现没数据,再查数据库,有数据,但是旧值。
  3. 请求c将数据库中的旧值,更新到缓存中。
  4. 此时,请求d卡顿结束,把新值写入数据库。
  5. 一段时间之后,比如:500ms,请求d将缓存删除。

这样来看确实可以解决缓存不一致问题。

那么,为什么一定要间隔一段时间之后,才能删除缓存呢?

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

此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了。

所以需要在请求d中加一个时间间隔,确保请求c,或者类似于请求c的其他请求,如果在缓存中设置了旧值,最终都能够被请求d删除掉。


4.先写数据库,再删缓存

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

那么,我们只能寄希望于最后的方案了。

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

  1. 请求e先写数据库,由于网络原因卡顿了一下,没有来得及删除缓存。
  2. 请求f查询缓存,发现缓存中有数据,直接返回该数据。
  3. 请求e删除缓存。

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

但如果是读数据请求先过来呢?

  1. 请求f查询缓存,发现缓存中有数据,直接返回该数据。
  2. 请求e先写数据库。
  3. 请求e删除缓存。

这种情况看起来也没问题呀?

答:对的。

但就怕出现下面这种情况,即缓存自己失效了。如下图所示:

  1. 缓存过期时间到了,自动失效。
  2. 请求f查询缓存,发缓存中没有数据,查询数据库的旧值,但由于网络原因卡顿了,没有来得及更新缓存。
  3. 请求e先写数据库,接着删除了缓存。
  4. 请求f更新旧值到缓存中。

这时,缓存和数据库的数据同样出现不一致的情况了。

其实先写数据库,再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即:如果缓存删除失败了,也会导致缓存和数据库的数据不一致。

那么,删除缓存失败怎么办呢?

答:需要加重试机制

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

当然,如果你在接口中直接同步重试,该接口并发量比较高的时候,可能有点影响接口性能。

这时,就需要改成异步重试了。

异步重试方式有很多种,比如:

  1. 每次都单独起一个线程,该线程专门做重试的工作。但如果在高并发的场景下,可能会创建太多的线程,导致系统OOM问题,不太建议使用。
  2. 将重试的任务交给线程池处理,但如果服务器重启,部分数据可能会丢失。
  3. 将重试数据写表,然后使用elastic-job等定时任务进行重试。
  4. 将重试的请求写入mq等消息中间件中,在mq的consumer中处理。
  5. 订阅mysql的binlog,在订阅者中,如果发现了更新数据请求,则删除相应的缓存。

4.解决方案

为了解决双写不一致问题,我们可以采取以下几种策略:

1.延迟双删

延迟双删是一种比较常用的解决方案。它的基本思路是:先删除缓存,然后更新数据库,最后再设置一个短暂的延时,等这个延时过去之后再删除一次缓存。这样可以确保在更新数据库和删除缓存之间的时间窗口内,即使有新的请求来查询数据,也会因为缓存中没有数据而去数据库中查询,从而保证了数据的一致性。

2.先更新数据库后删除缓存

另一种常见的解决方案是先更新数据库,然后再删除缓存。这种策略的好处是,即使更新缓存失败,由于数据库中的数据已经更新,所以数据仍然是一致的。但是,这种策略也有一个问题,那就是在更新数据库和删除缓存之间的时间窗口内,如果有新的请求来查询数据,可能会从缓存中读取到旧的数据。为了解决这个问题,我们可以采用一些额外的手段,比如给缓存数据设置一个较短的过期时间,或者在更新数据库时给数据项加上一个版本号,以便在查询时能够区分出最新的数据。

3.使用分布式锁

在某些场景下,我们可以使用分布式锁来保证在更新缓存和数据库时的一致性。具体做法是:在更新缓存和数据库之前先获取分布式锁,如果获取成功则进行更新操作,否则等待一段时间后重试。这样可以确保在同一时间只有一个操作在进行更新,从而避免了双写不一致的问题。但是,使用分布式锁也会带来一些额外的开销和复杂性,需要谨慎使用。

5.总结

双写不一致是分布式系统中常见的问题之一,特别是在使用缓存数据库时更容易出现。为了解决这个问题,我们可以采取一些策略,如延迟双删、先更新数据库后删除缓存、使用分布式锁等。在实际应用中,我们需要根据具体的场景和需求来选择合适的解决方案,并不断地优化和改进我们的系统以保证数据的一致性。

此外,为了避免双写不一致问题的发生,我们还应该在系统设计中注意以下几点:

  • 尽量减少缓存和数据库之间的数据同步需求。可以考虑将部分数据存储在缓存中,而另一部分数据则直接存储在数据库中,以减少同步操作。
  • 使用可靠的消息队列系统来保证数据的顺序性和一致性。通过将更新操作放入消息队列中进行异步处理,可以确保操作的顺序性和一致性。
  • 监控和报警。通过监控缓存和数据库之间的数据一致性情况,及时发现并处理双写不一致问题。同时,设置合适的报警机制以便在出现问题时能够及时得到通知并进行处理。

总之,双写不一致问题是分布式系统中需要重点关注和解决的问题之一。通过合理的系统设计和采取合适的解决方案,我们可以有效地避免和解决双写不一致问题,保证数据的正确性和一致性。

  • 49
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值