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

 
  数据库和缓存(redis)双写一致性问题,不管你用何种语言,尤其是在高并发的场景下,这个问题会很容易被放大。
  无论是在工作中,还是在面试中遇到这种问题的概率都非常大,因此在这里跟大家探讨下。
  方案一,也是最常见的方案
  通常,我们使用缓存是为了提高查询的性能,大多数情况下,我是用以下方案:

 

  1.   用户请求过来,先查缓存有没有数据,如果有直接返回,提高性能
  2.   如果缓存没有,再查询数据库
  3.   如果数据库有数据,则查询出来,放入缓存,供后续请求获取
  4.   如果数据库也没有,则返回空

   这个方案很常见,不仔细思考,好像没有问题,但是我们忽略了一个非常重要的细节,如果查询完数据库,放入缓存之后,这个数据又被立马更新了,那我们该如何更新缓存呢?

  如果不过更新,等缓存到过期时间,自动失效,那么这段时间,缓存和数据库就有数据不一致的问题。

  所以必须更新缓存,如何更新,目前有以下4个方案:

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

  接下来,讲一下这4种方案。

  先写缓存,再写数据库

  这个比较容易想到的方案,在更新请求后,直接更新缓存,我们先说下,先写缓存,再写数据库的情况。

 

  一个更新请求过来,如果刚写完缓存,发生了网络异常,导致写数据库失败了,结果就是缓存是最新数据,但是数据库没有,这样缓存就是脏数据,一致性问题就出现了。为了提高性能使用了缓存,但是现在缓存查出来的是假数据或者旧数据,就没有意义了,因此这个方案在生产中很少使用。

  

   先写数据库,再写缓存

   该方案在并发不高的项目中,比较常用。

 

 更新请求,先写数据库,在写缓存,可以避免上面方案中,出现假数据的问题。

如果写数据库,和写缓存在同一个事务了,当写缓存失败,我们会对数据回滚,这样会保证数据库缓存一致,这样在并发小的时候,对接口性能要求不高的系统,可以这么使用。

但是高并发业务场景中,写数据库和写缓存,都属于远程操作,为了防止出些大事务,造成死锁,通常情况下,建议写数据库和写缓存不要放在同一个事物中。

也就是说这个方案中,写数据库成功,但是写缓存失败,是不会回滚数据库的。

这样就出现缓存不一致的问题了。

如果觉得上面所说的情况太牵强,高并发情况下,还会出现以下问题 :

假设高并发场景,有两个写请求:a和b,他们同时请求到系统,其中a获取写入旧的数据,b是写入新的数据

1.a请求先到,刚写完数据库,由于网络问题,卡顿一下,还没来得及写缓存

2.b请求这个时候也到了,先写了数据库

3.接下来,b顺利写了缓存

4.此后,a卡顿结束,也写了缓存

很显然,数据库里面存的是b的新数据,而缓存却被a的旧数据覆盖了。

由此可见,在高并发的场景中,先写数据库,再写缓存,这套方案问题挺多的,也不太建议使用。

先删缓存,在写数据库

 上面的方案,直接更新缓存问题比较多。

那么,我们换一种思路,不去更新缓存,而是直接删缓存。

先看看,先删缓存的情况:

 

 这种虽然简单,但是也有会有一些问题:

高并发下的问题:

对同一个数据,同时有一个读请求c,还有一个更新请求d到系统,如图所示:

 

1.写请求d先到,把缓存删除,但由于网络原因,卡顿一下,还没来得及写数据库

2.这个时候查询请求c过来了,先查缓存发现没有数据,然后去查数据库,有值,但是是旧数据。

3.请求c将旧数据,更新到缓存中。

4.此时,写请求d卡顿结束,把新数据写入数据库。

在这个过程中,同样会导致数据库缓存不一致问题。(图中有问题,步骤7写入旧值,步骤9要删掉)

 那么怎么解决呢?

缓存双删,如下图所示:

 

这个方案不同之处,就是在写入数据库之前删除一次,写完数据后,再删除一次。

还有一个关键的地方,就是第二次删除,并非马上删除,而是要在一定时间间隔(如:500ms)之后删除。

这是为了保证第二次删除操作在写请求d卡顿结束,把新的值写入数据库后,查询请求c查到旧值写入缓存后执行。

 

先写数据库,再删除缓存

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

 

在高并发场景中,过程如下:

1.写请求e先写数据库,由于网路卡顿,没来得及删除缓存。

2.读请求f查询缓存,发现有数据,直接返回

3.写请求e删除缓存

这个过程,只有读请求f读了一次旧数据,后来e请求及时删除了缓存,看起来可以接受。

如果是还有另外一种情况呢?

1.读请求f查询缓存,发现有数据,直接返回。

2.写请求e先写数据库,再删除缓存。

看起来也没问题?

但是如果是下图显示的情况:

 

 

 

   1.刚好缓存过期,自动失效

  2.请求f从数据库获取旧值,写入缓存前,发生卡断

  3.写请求e先写数据库,再删除缓存

  4.f请求结束卡顿,把旧值写入缓存

这时,数据库和缓存也发生不一致的问题。

但这种情况还是比较少的,需要同时满足以下条件才可以:

    1. 缓存刚好自动失效。
    2. 请求f从数据库查出旧值,更新缓存的耗时,比请求e写数据库,并且删除缓存的还长。

由此可见,系统同时满足上述两个条件的概率非常小。

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

 删缓存失败怎么办?

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

那么就需要加重试机制。这个后面再开一篇讨论。

原文地址:https://mp.weixin.qq.com/s/mO819q_r9qLVuRBE8cjx1Q
posted @ 2022-08-26 15:04  梅晓煜  阅读(567)  评论(0编辑  收藏  举报