缓存和数据库一致性解决方案

一、先更新缓存再更新数据库方案

  • 假设线程一和线程二都要更新缓存和数据库(线程一将数据更新为a,线程二将数据更新为b)
  • 线程一先到达,然后线程一去更新了Redis缓存将缓存数据更新为a,然后准备更新数据库,此时由于某些原因(比如网络原因、程序发生GC等)导致线程一卡顿一下
  • 此时线程二到达,然后线程二完成对Redis缓存的更新和对数据库的更新,此时缓存中数据为b,数据库中的数据也为b
  • 然后线程一继续运行,接着去更新数据库,将数据库中的数据更新为a
  • 线程一和线程二都运行完毕。此时数据库和缓存的状态为:
    • 缓存中的数据为b
    • 数据库中的数据为a

通过上面的分析可以看到,当两个线程并发更新缓存和数据库时,如果采用先更新缓存然后再更新数据库的方案,有很大的几率会出现缓存不一致的情况。

二、先更新数据库再更新缓存方案

  • 假设线程一和线程二都要更新缓存和数据库(线程一将数据更新为a,线程二将数据更新为b)
  • 线程一先达到,首先将数据库的值更新为a,然后准备更新缓存,此时由于某些原因(比如网络原因、程序发生GC等)导致线程一卡顿一下
  • 此时线程二到达了,线程二将数据库的值更新为b,然后将缓存中的值更新为b,线程二运行结束(此时缓存中的数据为b,数据库中的数据为b)。
  • 线程一接着继续运行,然后线程一将缓存中的数据更新为a
  • 线程一也运行完毕,此时缓存和数据库中的数据状态为:
    • 缓存中的数据为a
    • 数据库中的数据为b

通过上面的分析可以看到,当两个线程并发更新缓存和数据库时,如果采用先更新数据库然后再更新缓存的方案,也会有很大的几率会出现缓存不一致的情况。

三、先删除缓存再更新数据库

既然更新缓存不行,那么我们直接删除缓存行不行?看下面分析

  • 假设最开始数据库和缓存中的值都是一致的(为 x )
    • 线程一需要更新数据库值为a
    • 线程二需要查询数据,先查询缓存,如果缓存查询不到那么就从数据库查,然后将查询结果缓存到Redis
  • 线程一先到达,首先将Redis缓存中的数据删除(此时缓存中的数据为null)
  • 接着线程二到达,开始执行查询动作
    • 线程二先查询缓存,由于缓存中的数据已经被线程一给删除了,所以线程二从缓存中无法获取到数据
    • 那么线程二开始去数据库查询数据,并将数据缓存到Redis缓存中。线程二运行结束
    • 此时Redis缓存中的数据为 x,数据库中的数据也为 x
  • 线程一接着运行,开始更新数据库数据为a,线程一运行结束
  • 此时缓存和数据库中的数据状态为:
    • 缓存中的数据为 x
    • 数据库中的数据为a

通过上面的分析可以看到,采用先删缓存然后再更新数据库的方案,也会有很大的几率会出现缓存不一致的情况。

四、先写库再删除缓存方案

这种方案是我们在项目中采用的最多的一种方案。虽然这种方案仍然有几率会出现缓存一致性问题,但是这种几率相比之下会非常小,那么这种方案在什么情况下会出现缓存一致性问题呢?看下面分析

  • 场景假设
    • 最开始数据库的值为x,缓存中的值为空(可能是刚好这个key的过期时间到了)
    • 线程一需要更新数据库值为a
    • 线程二需要查询数据,先查询缓存,如果缓存查询不到那么就从数据库查,然后将查询结果缓存到Redis
  • 线程二先到达,先查询缓存,没查到数据,然后查询数据库(查询得到的数据为x)。然后执行数据更新到缓存动作(恰巧此时由于某种原因,线程二卡顿了一下
  • 此时线程一到达,先执行数据库更新,将数据库中的值更新为 a,然后执行删除缓存的动作。线程一执行完毕。
  • 线程二接着恢复运行,继续将刚才查询到的数据(x)更新到缓存中。线程一运行结束
  • 此时缓存和数据库中的数据状态为:
    • 缓存中的数据为 x
    • 数据库中的数据为a

通过上面的分析可以看到,采用先更新库然后再删除缓存的方案,理论上也会出现缓存不一致的情况。但是为什么说这种方案出现缓存一致性问题的几率会相对小很多呢?这是因为它必须满足 3 个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 更新数据库 + 删除缓存的时间,要比读数据库 + 写缓存时间短

仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。【所以,我们应该采用这种方案,来操作数据库和缓存】

五、延时双删方案

延时双删是指在先写库再删缓存方案中再增加一次删除动作,而这次删除动作并不是立即删除,而是在指定的延时时间后再次删除缓存。借此来【尽最大可能保证缓存一致性】,将缓存和数据库中数据不一致的几率降到最低。

①、问题思考

  • 先写库再删缓存方案中,我们发现仍然有很小的几率可能会出现缓存一致性问题,所以我们能不能再进行改进呢?
  • 于是我们就想,可不可以在【写库-删缓存】动作之后延迟个一两秒再删除一次缓存。这样即使线程二更新了旧数据到缓存中,那么该旧数据在延时时间到达时仍然会被删除。这样就可以保证后面到来的线程不会产生缓存一致性问题。

②、延时双删方案

  • 对于要更新数据的动作,首先更新数据库
  • 然后删除缓存
  • 然后投递一个延时消息到MQ中
  • 当延时消息延时时间到了以后,触发再次删除缓存。

③、其他问题

1、一般采用什么方案来做延时执行呢

其实方案有很多,最常用的一般是使用MQ延时消息来执行延时删除动作。

2、延时双删延迟时间设置多长

一般是根据自己的业务场景定的,按照自己的业务场景选择合适的延时时间。

3、有没有什么方案能保证数据库和缓存强一致性呢

没有,如果一定要保证数据库和缓存的强一致性,那么只有使用分布式锁了。分布式锁会降低系统的并发度,那么缓存存在的意义其实也不大了。

4、生产中一般建议采用什么方案呢

我们一般使用的最多的是[先写库再删缓存]方案,该方案很小几率会出现缓存一致性问题。对于延时双删方案虽然尽最大努力保证一致性,但是这种方案需要引入MQ,复杂度也比较高,所以一般采用的也不多。如果要保证强一致性,那么建议使用分布式锁,牺牲并发来换取强一致性。或者直接就不使用缓存。

猜你喜欢

转载自juejin.im/post/7126422435116613663