其他

杂谈:如何保证Redis缓存和数据库数据的一致性?

如果我们使用 Redis 做MySQL等数据库的缓存层,就必然会面对缓存和数据库间的一致性保证问题,这也算是 Redis 缓存应用中的“必答题”了。

Redis常用作MySQL等数据库的缓存层,如何保证Redis缓存和数据库数据的一致性?


听到这个问题,你心里一阵窃喜,这个研究过,网上的文章也给过答案。

于是告诉面试官方案如下:

  • 1、写操作时:先更新数据库,再清除缓存;
  • 2、读操作:读取缓存,存在则直接返回,不存在则读取数据库,之后更新到缓存。

为什么是删除缓存,而不是更新缓存呢?


更新缓存会有并发问题,可能会导致缓存与数据库数据不一致,这对大多数业务场景来说是不能接受的。如线程1和2都是写操作,线程1先完成数据库写操作,然后线程2完成了数据库和缓存的写操作,之后线程1完成缓存写操作,那么此时缓存和数据库的数据就不一致了。

为什么不是先删除缓存,再更新数据库呢?


先删除缓存、再更新数据库容易造成读写请求并发问题,可能造成数据不一致。另外,先删除缓存,由于缓存中数据缺失,加剧数据库的请求压力,可能会增大缓存穿透出现的概率。

数据不一致的场景是:线程1删除缓存后,线程2读取到数据库旧值,之后更新旧值到缓存中,之后线程1更新数据库,造成缓存不一致。

因为写数据库往往慢于读请求,所以此问题出现的概率还是相对较大的。

如果使用此方案,业界也提出了“延迟双删”的方案解决不一致问题,即在更新数据库后,再操作一次删除缓存。为了保证第二次删除缓存的时间点在读请求更新缓存之后,这个延迟时间的经验值通常应稍大于业务中读请求的耗时。延迟的实现可以在代码中 sleep 或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延迟双删被诟病的主要原因。延迟双删的流程如下图所示[1]

那当前这个方案在并发读写的时候会有数据不一致的问题吗?


当前方案出现数据不一致问题的概率很小,出现的条件极其严苛。如果需要强一致性,也是需要加分布式锁的,但是这样的话,方案的复杂性就会大大增加了。

类比上述读写并发的场景,线程1读请求,此时缓存刚好失效了,就从数据库中读取了旧值,然后线程2更新数据库并操作清除了缓存,之后线程1更新旧值到缓存中。如下图所示

由上图可知,出现数据不一致问题的条件包括:

  • (1)线程1读数据时,无缓存;
  • (2)线程1读请求、线程2写请求并发;
  • (3)线程1更新缓存比线程2更新数据库+删除缓存加起来耗时都长。

可见,出现该问题的条件还是比较苛刻的,尤其是第(3)个条件,一般情况下更新数据库都是比更新缓存要慢的,除非刚好线程1到缓存服务刚好出现网络抖动,才会出现该问题。

还有一点,需要指出:上述谈到的数据不一致都是缓存与数据库中的数据可能由于并发等问题导致的长时间不一致,避免该问题,即达成了缓存与数据库数据的最终一致性。

如果需要缓存与数据库数据的强一致性,必然要把操作数据库和缓存放置到同一事务中,操作资源时也需要加分布式锁避免并发读写造成的不一致,这也会导致方案复杂度的上升和请求性能的下降。