一、先更新缓存再更新数据库方案
- 假设线程一和线程二都要更新缓存和数据库(线程一将数据更新为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 个条件:
- 缓存刚好已失效
- 读请求 + 写请求并发
- 更新数据库 + 删除缓存的时间,要比读数据库 + 写缓存时间短
仔细想一下,条件 3 发生的概率其实是非常低的。
因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。这么来看,「先更新数据库 + 再删除缓存」
的方案,是可以保证数据一致性的。【所以,我们应该采用这种方案,来操作数据库和缓存】。
五、延时双删方案
延时双删是指在先写库再删缓存方案
中再增加一次删除动作,而这次删除动作并不是立即删除,而是在指定的延时时间后再次删除缓存。借此来【尽最大可能保证缓存一致性】
,将缓存和数据库中数据不一致的几率降到最低。
①、问题思考
- 在
先写库再删缓存方案
中,我们发现仍然有很小的几率可能会出现缓存一致性问题
,所以我们能不能再进行改进呢? - 于是我们就想,可不可以在
【写库-删缓存】
动作之后延迟个一两秒再删除一次缓存。这样即使线程二
更新了旧数据到缓存中,那么该旧数据在延时时间到达时仍然会被删除。这样就可以保证后面到来的线程不会产生缓存一致性问题。
②、延时双删方案
- 对于要更新数据的动作,首先更新数据库
- 然后删除缓存
- 然后投递一个延时消息到MQ中
- 当延时消息延时时间到了以后,触发再次删除缓存。
③、其他问题
1、一般采用什么方案来做延时执行呢
其实方案有很多,最常用的一般是使用MQ延时消息来执行延时删除
动作。
2、延时双删延迟时间设置多长
一般是根据自己的业务场景定的,按照自己的业务场景选择合适的延时时间。
3、有没有什么方案能保证数据库和缓存强一致性呢
没有,如果一定要保证数据库和缓存的强一致性,那么只有使用分布式锁了。分布式锁会降低系统的并发度,那么缓存存在的意义其实也不大了。
4、生产中一般建议采用什么方案呢
我们一般使用的最多的是[先写库再删缓存]
方案,该方案很小几率会出现缓存一致性问题。对于延时双删
方案虽然尽最大努力保证一致性,但是这种方案需要引入MQ,复杂度也比较高,所以一般采用的也不多。如果要保证强一致性,那么建议使用分布式锁,牺牲并发来换取强一致性。或者直接就不使用缓存。