精华内容
下载资源
问答
  • 缓存双写一致性
    千次阅读
    2022-04-12 15:47:11

    前言:

    在开头有必要说明,如果对数据一致性要求比较高就不要存缓存,因为只要涉及到双写就一定存在一致性问题。

    1. 缓存基本使用方式

      如果可以容忍数据不一致话,我们可以给缓存设置一个过期时间,所有写操作以数据库为基准,缓存过期后就会去数据库中取新值,保证了数据的最终一致性

    在这里插入图片描述

      过期时间也就是我们能容忍数据最大不一致的时间。过期时间太短,数据不一致时间短,但是读数据库频繁;过期时间太长,数据不一致时间也就越长。

    1.1 增大缓存过期时间,数据变更主动更新缓存

      增大缓存过期时间,能有效减少读DB频繁问题,但是会造成数据不一致时间过长,这时候我们得再数据变更后主动更新缓存,来减少不一致时间。

    这里我们讨论三种更新策略:

    • 先更新数据库,再更新缓存
    • 先删除缓存,再更新数据库
    • 先更新数据库。再删除缓存

    2. 先更新数据库,再更新缓存

      假设我们采用【先更新数据库,再更新缓存】策略,并且这两步都可以成功执行前提下。如果存在并发,问题会咋样?

    2.1 线程安全问题

      现有2个并发请求,请求A和请求B。

    1. 请求A更新数据库(x = 1)
    2. 请求B更新数据库(x = 2)
    3. 请求B更新缓存(x = 2)
    4. 请求A更新缓存(x = 1)

      最后我们发现,数据库中的 x 是2,而缓存中是1。显然是不一致的,缓存中的数据是“脏”的。

    2.2 从业务场景角度考虑

    1. 如果业务是写多读少,每次对数据库写操作后,都去做一次缓存更新操作,显然产生了不必要的开销

    该缓存策略暂不考虑

    3. 先删除缓存,再更新数据库

      假设我们采用【先删除缓存,再更新数据库】策略,并且这两步都可以成功执行前提下。如果存在并发,问题会咋样?

    3.1 线程安全问题

      现有2个并发请求,请求A和请求B。

    1. 请求A删除缓存
    2. 请求B发现缓存不存在
    3. 请求B去数据库读取旧值(x = 1)
    4. 请求B将旧值写入缓存(x = 1)
    5. 请求A更新数据库(x = 2)

      上述情况就会导致数据库的值已经被更新,而缓存中的值仍然是旧值的情况。如果不给缓存设置过期时间的话,缓存中的数据永远是“脏”的数据。

    3.2 延时双删策略

    那么有什么解决办法吗?

      可以采用延时双删策略。其实就是请求A在更新完数据库后,延迟一会时间,然后进行删除缓存操作。具体延迟多久得看具体的业务耗时来定。同步的去淘汰缓存,只会导致吞吐量降低,因此可以将第二次删除以异步的方式来处理。

    问题一

      即便是采取了双删策略,延时时间过短,也不能保证数据一定会一致。

    1. 请求A删除缓存
    2. 请求B发现缓存不存在
    3. 请求B去数据库读取旧值(x = 1)
    4. 请求A更新数据库(x = 2)
    5. 请求A删除缓存(此时请求B还没写入缓存)
    6. 请求B将旧值写入缓存(x = 1)

      发生这种情况主要原因:延时时间过短。

    问题二

      数据库采用了读写分离架构,且同步方案是半同步机制 or 异步复制机制,那么这个延迟时间就要设置更长了

    1. 请求A删除缓存

    2. 请求A更新数据主库(x = 2)

    3. 请求B发现缓存不存在

    4. 请求B去数据从库读取旧值(x = 1)

    5. 数据库完成主从同步,数据从库数据更新(x = 2)

    6. 请求B将旧值写入缓存(x = 1)

      为了解决不一致的问题,可以简单粗暴的增长延时时间。或者数据库采用全同步机制,当然生产环境一般不这样做,mysql性能会受到严重影响。或者读写都在主库,也会影响数据库性能。

    4. 先更新数据库,再删除缓存

      这种策略也会存在并发问题吗?

    1. 缓存A刚好失效
    2. 请求A查询数据库,得到一个旧值(x = 1)
    3. 请求B将新值写入数据库
    4. 请求B删除缓存
    5. 请求A将旧值写入到缓存(x = 1)

      如果发生上述情况,确实是会有脏数据存在。不过发生的概率很低,比【先删除缓存,再更新数据库】策略要低。

    4.1 主动更新缓存,为啥缓存还要过期时间?

    这里插入一个问题?

      我都采用主动更新缓存策略了,为啥缓存要设置过期时间,我不设置可以吗?

    • 缓存是非常昂贵的,对于热点 key 转为冷 key,它们没理由还保存在内存中,如果不设置过期时间,它将永远保存在内存中。
    • 当然你的业务都是热点 key,就可以不设置缓存过期时间,也就不会出现上面的并发问题;甚至所有 key 都可以不设置过期时间,当变成冷 key 后,运维去主动监控、删除,请问你公司的运维会答应吗?

      再回到主题,我们知道上述并发情况一般很少出现,但是如果非要解决这个问题,怎么做?

    • 缓存设置过期时间:缓存过期时间后去取新值,保证最终一致性。(我们采用主动更新缓存策略,就是为了增大过期时间,减少对DB读压力,这里缓存过期虽然可以保证最终一致性,但是会导致不一致时间很长)
    • 延时双删策略:在删除缓存后,延迟一段时间,然后再次删除缓存
    • 保障的重试机制:延时双删会有一个问题,如果第二次删除失败,仍然会出现不一致

    5. 最终方案

      我们可以采用 【先更新数据库,再删除缓存】+ 【延时双删策略】+ 【缓存过期】+ 【保障重试机制】来保证缓存最终一致性

    在这里插入图片描述

      该方案有个缺点,对业务代码造成大量的侵入。每个更新、删除数据的业务代码都得接入。于是有了了另一种方案,启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。在应用程序 中,获取该订阅程序传递来的消息,进行删除缓存操作。

    在这里插入图片描述
      这里你会想为啥要第一次删除呢?既然第一次删除可能出行数据不一致。

      数据不一致是有个时间间隔,第一次删除的意义在于可以减少这个间隔,修改成功后,第一次删除保证缓存立马失效去读新的数据。

    缺点

    • 不适合“秒杀”这种频繁修改数据和要求数据强一致的场景,多次删除会导致 nsq 消息多,且删除缓存频繁。(甚至可以理解为要求强一致、更新频繁就不应该用缓存)
    更多相关内容
  • 本文由以下三个部分组成1、讲解缓存更新策略2、对每种策略进行缺点分析3、针对缺点给出改进方案先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的...
  • 分布式缓存是现在很多分布式应用中必不可少的组件,但是用到了分布式缓存,就可能会涉及到缓存与数据库存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? Cache Aside Pattern 最...
  • 如何保证数据库和缓存双写一致性

    千次阅读 2022-04-03 17:00:01
    数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。 接下来我和大家一起来探讨一下 常见方案 通常情况下,我们使用缓存的主要目的是...

    数据库和缓存(比如:redis)双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。
    接下来我和大家一起来探讨一下

    常见方案

    通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们是这样使用缓存的:
    在这里插入图片描述

    简单来说就是先查缓存,如果缓存中没有数据再去查数据库,同时将数据库中查出的结果更新同步至缓存,如果数据库中也没有,则返回空。
    这种缓存用法表面上看着很合理,可是我们忽略了一些重要的场景

    如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?

    不更新缓存行不行?

    答案是肯定不行,要不然本文也就没有存在的必要了。
    如果长期不更新,只依赖于缓存的过期时间的话,那么用户可能在很长一段时间内使用的都是旧的数据,比如:如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么就会出现上面这个情况。

    那么,我们该如何更新缓存呢?
    目前有以下4种方案:

    • 先写缓存,再写数据库
    • 先写数据库,再写缓存
    • 先删缓存,再写数据库
    • 先写数据库,再删缓存

    我们看看这些方案到底行不行的通

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

    我们知道缓存快的主要原因是其 I/O的瓶颈,由于缓存直接读写内存,所以操作速度很快,但是直接读写内存的话,如果遇到缓存数据库宕机,就会导致写入到内存数据丢失,同时我们还会遇到当写入缓存成功之后,如果刚写完缓存,突然网络出现了异常,导致写数据库失败了,这种情况是缓存有,数据库没有,此时缓存中的数据就变成了脏数据。
    我们必须要理解缓存的主要目的是暂存,也就是说把数据库的数据临时保存在内存,便于后续的查询,提升查询速度,但如果某条数据,在数据库中都不存在,那这条数据可以说是毫无意义。因此这种方案不可取。

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

    这种方案可以避免假数据带来的问题,所谓假数据就是指数据库中没有,但是缓存中有的数据就是假数据。但是新的问题又产生了。

    2.1 写缓存失败了

    如果把写数据库和写缓存操作,放在同一个事务当中,当写缓存失败了,我们可以把写入数据库的数据进行回滚。
    这种场景也可以接受,但是只能应用于并发量较小的业务场景,对接口性能要求不太高的系统,可以这么做。
    但如果在高并发的业务场景中,写数据库和写缓存,都属于远程操作。为了防止出现大事务,造成的死锁问题,通常建议写数据库和写缓存不要放在同一个事务中。
    如果同时写入缓存和数据库就会出现写数据库成功了,但写缓存失败了,数据库中已写入的数据不会回滚,导致数据库是新数据,而缓存是旧数据,两边数据不一致的情况。

    2.2 高并发场景

    假设在高并发场景下,针对同一个用户的同一条数据,有两个写数据请求:a和b,它们同时请求到业务系统,其中请求a获取的是旧数据,而请求b获取的是新数据,如下图所示:
    在这里插入图片描述

    我么就按照上图的流程走一遍:

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

    上面的过程就会出现一个很严重的情况:数据库中是新值,而数据库中是旧值。

    而且这种方式会严重浪费系统资源。
    为什么这么说呢?

    如果写的缓存,并不是简单的数据内容,而是要经过非常复杂的计算得出的最终结果。这样每写一次缓存,都需要经过一次非常复杂的计算,不是非常浪费系统资源吗!!!

    尤其是当我们遇到的业务场景是写多读少,在这类业务场景下,每一次写操作都需要写一次缓存,这样的话就有点得不偿失了。

    3.先删缓存在写数据库

    上面两种方案我们可以看到直接更新缓存的问题很多。
    所以我们换一种思路:不去直接更新缓存,而改为删除缓存
    但是删缓存也有两种方案:

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

    3.1 先删缓存,在写数据库

    大致流程如下:
    在这里插入图片描述

    这个流程有没有问题呢?
    我们一起来分析一下
    还是先讨论一下高并发下的问题

    3.1.1 高并发下的问题

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

    上图流程如下:

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

    在这个过程当中,请求d的新值并没有被请求c写入缓存,同样会导致缓存和数据库的数据不一致的情况。

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

    3.1.2 缓存双删

    针对上面这种场景有一种很简单的处理办法,思路很简单:
    当d请求写入成功之后,我们在将缓存重删一次。
    在这里插入图片描述

    这就是我们所说的缓存双删,即:

    即在写数据库之前删除一次,写完数据库后,再删除一次。

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

    有了缓存删除方案之后,我们在回顾一下高并发下的场景问题:

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

    这样看确实解决了缓存不一致的问题,但是为什么我们非得等一会在删除缓存呢?
    请求d卡顿结束,把新值写入数据库后,请求c将数据库中的旧值,更新到缓存中。
    此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。我们必须要搞清楚,我们之所以要再删除一次缓存的原因是因为c请求导致缓存中更新了数据库中旧值,我们需要把这个旧值删除掉,所以必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了,删除删除太快,可能后面。

    现在解决了一个问题之后,又遇到一个问题:如果第二次删除缓存时,删除失败了该怎么办呢?
    由于下面的场景同样也会遇到这个问题,所以我单独拿出来讲解缓存删除失败的解决方案。

    3.2 先写数据库,再删缓存

    从前面得知,先删缓存,再写数据库,在并发的情况下,也可能会出现缓存和数据库的数据不一致的情况。
    接下来,我们重点看看先写数据库,再删缓存的方案。
    在高并发的场景中,有一个读数据请求,有一个写数据请求,更新过程如下:

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

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

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

    这种情况有问题吗?
    完全没问题!!!
    但是呢,我们别忘了,我们的缓存如果设置了有效期,即缓存 自己失效了。
    在这里插入图片描述

    上面的流程大致如下:

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

    当然这种情况发生的概率比较下,只有同时满足:

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

    查询数据库的速度,一般比写数据库要快,更何况写完数据库,还要删除缓存。所以绝大多数情况下,写数据请求比读数据情况耗时更长。

    我们先做个总结:

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

    但是这个方案也会也到缓存删除失败的情况,解决缓存删除失败的情况

    4.删缓存失败怎么办?

    如果缓存删除失败了,也会导致缓存和数据库的数据不一致。
    所以为了解决这个方案,我们加入一个重试机制
    在接口中如果更新了数据库成功了,但更新缓存失败了,可以立刻重试3次。如果其中有任何一次成功,则直接返回成功。如果3次都失败了,则写入数据库,准备后续再处理。

    当然,如果你在接口中直接同步重试,该接口并发量比较高的时候,可能有点影响接口性能,这个我们不怕,可以改为异步。
    异步重试方式有很多种:

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

    4.1 定时任务

    我们创建一个重试表,表中有个字段记录重试次数,初始值为0,同时设置一个最大的重试次数,用一个定时任务异步的去读取重试表中的数据,然后去执行删除缓存操作,每删除一次,重试次数加1,如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则我们需要在重试表中记录一个失败的状态,等待后续进一步处理。
    在高并发场景中,定时任务推荐使用elastic-job。相对于xxl-job等定时任务,它可以分片处理,提升处理速度。同时每片的间隔可以设置成:1,2,3,5,7秒等。

    使用定时任务重试的话,有个缺点就是实时性没那么高,对于实时性要求特别高的业务场景,该方案不太适用。但是对于一般场景,还是可以用一用的。

    但它有一个很大的优点,即数据是落库的,不会丢数据。

    4.2 MQ

    在高并发的业务场景中,mq(消息队列)是必不可少的技术之一。它不仅可以异步解耦,还能削峰填谷。对保证系统的稳定性是非常有意义的。

    mq的生产者,生产了消息之后,通过指定的topic发送到mq服务器。然后mq的消费者,订阅该topic的消息,读取消息数据之后,做业务逻辑处理。

    使用mq重试的具体方案如下:
    在这里插入图片描述

    当用户操作写完数据库,但删除缓存失败了,产生一条mq消息,发送给mq服务器。
    mq消费者读取mq消息,重试5次删除缓存。如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则写入死信队列中。
    当然在该方案中,删除缓存可以完全走异步。即用户的写操作,在写完数据库之后,不用立刻删除一次缓存。而直接发送mq消息,到mq服务器,然后有mq消费者全权负责删除缓存的任务。

    因为mq的实时性还是比较高的,因此改良后的方案也是一种不错的选择。

    4.3 binlog

    前面两种删除的重试方案 都有一定的侵入性:

    • 在使用定时任务的方案中,需要在业务代码中增加额外逻辑,如果删除缓存失败,需要将数据写入重试表。
    • 使用mq的方案中,如果删除缓存失败了,需要在业务代码中发送mq消息到mq服务器。

    其实,还有一种更优雅的实现,即监听binlog,比如使用:canal等中间件。
    具体方案如下:
    在这里插入图片描述

    • 在业务接口中写数据库之后,就不管了,直接返回成功。
    • mysql服务器会自动把变更的数据写入binlog中。
    • binlog订阅者获取变更的数据,然后删除缓存。

    这套方案中业务接口确实简化了一些流程,只用关心数据库操作即可,而在binlog订阅者中做缓存删除工作。
    但是呢,这种方案还会出现删除失败的情况,因为基于binlog实现的删除只会删除一次,所以我们最终还是需要依赖于基于定时任务或者mq的重试机制。
    在binlog订阅者中如果删除缓存失败,则发送一条mq消息到mq服务器,在mq消费者中自动重试5次。如果有任意一次成功,则直接返回成功。如果重试5次后还是失败,则该消息自动被放入死信队列,后面可能需要人工介入。
    在这里插入图片描述

    展开全文
  • 数据库和缓存双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。 一、简介 一般情况下,使用缓存都是为了提升查询的性能(redis 单机支持 10万 QPS),减轻DB访问...

    数据库和缓存双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。

    一、简介

    一般情况下,使用缓存都是为了提升查询的性能(redis 单机支持 10万 QPS),减轻DB访问压力。对于查询请求,引入缓存之后的流程通常如下:
    在这里插入图片描述

    1. 用户请求到达服务器,首先去缓存查询。如果缓存命中,直接返回;缓存没命中,下一步;
    2. 去数据库查询,如果数据不存在,直接返回(是否缓存空值取决于实际业务);如果数据存在,更新缓存,返回结果。

    但是,如果在高并发的情况下,某条记录在被放入缓存之后,又立马被更新了,此时需要跟着将缓存中的数据更新,目前有四种方案:

    • 先更新数据库,后更新缓存;
    • 先更新缓存,后更新数据库;
    • 先删除缓存,再更新数据库;
    • 先更新数据库,再删缓存。

    二、先更新数据库,后更新缓存

    如果一开始就先去更新数据库,更新成功之后,再去更新缓存;更新失败直接返回。
    在这里插入图片描述

    此种方案存在以下问题:

    1. 更新缓存的代价很高。如果此时有大量的写请求,但是读请求并不多,如果每次写请求都更新一下缓存,那么性能损耗是非常大的而且中间的很多次更新也是没有必要的;
    2. 如果此时有多个并发写请求,会有几率出现数据不一致的情况。
      暂时无法在文档外展示此内容

    三、先更新缓存,后更新数据库

    此种方案跟先更新数据库后更新缓存一样,会存在同样的问题。不仅如此,此种方案还有更加严重的问题:生产中,所有的核心数据一定是要入DB保存的,缓存中存放的数据都是能够接受一定程度的缓存不一致性的数据,如果缓存更新成功之后,更新数据库失败了,就会导致缓存与DB数据不一致,但是DB是没有保存真正的更改后的数据的,一旦缓存失效了,对应的数据也会丢失,此方案是一定不会被采用的。

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

    在用户的写操作中,先执行删除缓存操作,再去更新数据库。
    在这里插入图片描述

    此种方案存在以下问题:

    1. 在高并发的场景中,同一个用户的同一条数据,有一个读数据请求,还有另一个写数据请求(一个更新操作),同时请求到业务系统。如下图所示:
      在这里插入图片描述

    在上面的业务场景中,一个读请求,一个写请求。当写请求把缓存删了之后,读请求,可能把当时从数据库查询出来的旧值,写入缓存当中。为了解决这种情况导致的数据不一致,可以在写请求更新了DB之后,再次删除缓存,这就是缓存延迟双删。
    在这里插入图片描述

    缓存延迟双删关键的地方是:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后(这样才能保证将读请求设置的旧的缓存值干掉)。

    五、先更新数据库,再删缓存

    写操作,先去更新DB,然后再删除缓存。
    在这里插入图片描述

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

    1. 写请求先写数据库,由于网络原因卡顿了一下,没有来得及删除缓存;
    2. 读请求查询缓存,命中缓存,直接返回缓存中数据;
    3. 写请求删除缓存,后续读请求查询DB,设置缓存后返回数据。
      在这个过程中,只有第一次读请求读了一次旧数据,后来旧数据被写请求及时删除了,看起来问题不大。

    另一种情况,读请求先到达服务器:

    1. 读请求查询缓存,命中缓存,直接返回缓存中数据;
    2. 写请求先写数据库,然后删除缓存。
      在这种情况下,也不会出现问题。

    但是存在另一种情况,缓存自己过期失效:
    在这里插入图片描述

    但是上述情况出现的几率是很小的,需要同时满足以下两个条件:

    1. 缓存刚好到了过期时间,失效;
    2. 读请求从DB查询数据,更新缓存的耗时比写请求写DB和删除缓存的时间要长(查询数据库的速度,一般比写数据库要快,更何况写完数据库,还要删除缓存。所以绝大多数情况下,写数据请求比读数据情况耗时更长)。

    在上面的业务场景中,当缓存刚好过期且读请求更新缓存耗时比写请求写DB和删除缓存的耗时更长,还是会出现缓存不一致的情况。为了解决这种情况导致的数据不一致,可以在写请求更新了DB,删除缓存之后,再次删除缓存(跟延迟双删一样,间隔一段时间后再次删除缓存)。

    六、缓存删除失败如何处理?

    不管是先更新数据库,再删除缓存,还是基于先删除缓存,再更新数据库改进来的延时双删,都存在一个问题:一旦缓存删除失败,DB和缓存是数据就会不一致。
    解决缓存删除失败的方法很简单:添加重试机制。
    在接口中如果更新了数据库成功了,但删除缓存失败了,可以立刻重试3次。如果其中有任何一次成功,则直接返回成功。如果3次都失败了,则写入数据库,准备后续再处理。
    如果在接口中直接同步重试,该接口并发量比较高的时候,可能有点影响接口性能。推荐使用异步重试,异步重试的方式可以有很多种:

    1. 将重试的任务交给线程池处理,但是如果服务不采取优雅停服机制,线程池中的任务存在丢失的情况;
    2. 将重试数据写表,然后使用elastic-job等定时任务进行重试;
    3. 将重试的请求写入mq等消息中间件中,在mq的consumer中处理;
    4. 订阅mysql的binlog,在订阅者中,如果发现了更新数据请求,则删除相应的缓存。

    1、定时任务重试

    当用户操作写完数据库,但删除缓存失败了,需要将数据写入重试表中。流程如下图所示:
    在这里插入图片描述

    在定时任务中,异步读取重试表中的数据。重试表需要记录一个重试次数字段,初始值为0。然后重试5次,不断删除缓存,每重试一次该字段值+1。如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则需要在重试表中记录一个失败的状态,等待后续进一步处理。
    在这里插入图片描述

    使用定时任务重试的话,有个缺点就是实时性没那么高,对于实时性要求特别高的业务场景,该方案不太适用。但是对于一般场景,还是可以用一用的。但它有一个很大的优点,即数据是落库的,不会丢数据。

    2、消息队列

    使用消息队列实现缓存删除的方案如下:

    1. 当用户操作写完数据库,产生一条mq消息,发送给mq服务器;
    2. mq消费者读取mq消息,重试5次删除缓存。如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则写入死信队列中。
      在这里插入图片描述

    3、订阅binlog

    无论是定时任务,还是mq(消息队列),做重试机制,对业务都有一定的侵入性:

    • 在使用定时任务的方案中,需要在业务代码中增加额外逻辑,如果删除缓存失败,需要将数据写入重试表;
    • 使用mq的方案中,如果删除缓存失败了,需要在业务代码中发送mq消息到mq服务器。

    还有一种更优雅的实现,即监听binlog,比如使用:canal等中间件。具体流程如下:

    1. 在业务接口中更新数据库之后,直接返回成功;
    2. mysql服务器会自动把变更的数据写入binlog日志中;
    3. binlog订阅者获取变更的数据,然后删除缓存;
    4. 如果删除缓存失败,不断重试(推荐使用MQ),直到成功。
      在这里插入图片描述
    展开全文
  • MySQL数据库与Redis缓存双写一致性

    千次阅读 多人点赞 2022-04-02 13:57:42
    MySQL数据库与Redis缓存双写一致性 文章目录MySQL数据库与Redis缓存双写一致性问题分析一致性问题是如何产生的?双更新模式:操作不合理,导致数据一致性问题不再更新缓存,直接删除,为什么?“后删缓存”能解决...

    MySQL数据库与Redis缓存双写一致性

    问题

    你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

    分析

    先做一个说明,从理论上来说,有两种处理思维,一种需保证数据强一致性,这样性能肯定大打折扣;另外我们可以采用最终一致性,保证性能的基础上,允许一定时间内的数据不一致,但最终数据是一致的。

    一致性问题是如何产生的?

    对于读取过程:

    • 首先,读缓存;
    • 如果缓存里没有值,那就读取数据库的值;
    • 同时把这个值写进缓存中。

    双更新模式:操作不合理,导致数据一致性问题

    我们来看下常见的一个错误编码方式:

    public void putValue(key,value){
        // 保存到redis
        putToRedis(key,value);
        // 保存到MySQL
        putToDB(key,value);//操作失败了
    }
    

    比如我要更新一个值,首先刷了缓存,然后把数据库也更新了。但过程中,更新数据库可能会失败,发生了回滚。所以,最后“缓存里的数据”和“数据库的数据”就不一样了,也就是出现了数据一致性问题。

    image.png

    你或许会说:我先更新数据库,再更新缓存不就行了?

    public void putValue(key,value){
        // 保存到MySQL
        putToDB(key,value);
        // 保存到redis
        putToRedis(key,value);
    }
    

    这依然会有问题。

    考虑到下面的场景:操作 A 更新 a 的值为 1,操作 B 更新 a 的值为 2。由于数据库和 Redis 的操作,并不是原子的,它们的执行时长也不是可控制的。当两个请求的时序发生了错乱,就会发生缓存不一致的情况。

    image.png

    放到实操中,就如上图所示:A 操作在更新数据库成功后,再更新 Redis;但在更新 Redis 之前,另外一个更新操作 B 执行完毕。那么操作 A 的这个 Redis 更新动作,就和数据库里面的值不一样了。

    那么怎么办呢?其实,我们把“缓存更新”改成“删除”就好了

    不再更新缓存,直接删除,为什么?

    • 业务角度考虑

    原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

    • 性价比角度考虑

    更新缓存的代价有时候是很高的。如果频繁更新缓存,需要考虑这个缓存到底会不会被频繁访问?

    举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

    “后删缓存”能解决多数不一致

    因为每次读取时,如果判断 Redis 里没有值,就会重新读取数据库,这个逻辑是没问题的。

    唯一的问题是:我们是先删除缓存?还是后删除缓存?

    答案是后删除缓存。

    1.如果先删缓存

    我们来看一下先删除缓存会有什么问题:

    public void putValue(key,value){
        // 删除redis数据
        deleteFromRedis(key);
        // 保存到数据库
        putToDB(key,value);
    }
    

    image.png

    就和上面的图一样。操作 B 删除了某个 key 的值,这时候有另外一个请求 A 到来,那么它就会击穿到数据库,读取到旧的值, 然后写入redis,无论操作 B 更新数据库的操作持续多长时间,都会产生不一致的情况。

    2.如果后删缓存

    而把删除的动作放在后面,就能够保证每次读到的值都是最新的。

    public void putValue(key,value){
        // 保存到数据库
        putToDB(key,value);
        // 删除redis数据
        deleteFromRedis(key);
    }
    

    这就是我们通常说的Cache-Aside Pattern,也是我们平常使用最多的模式。我们看一下它的具体方式。

    先看一下数据的读取过程,规则是“先读 cache,再读 db”,详细步骤如下:

    • 每次读取数据,都从 cache 里读;
    • 如果读到了,则直接返回,称作 cache hit;
    • 如果读不到 cache 的数据,则从 db 里面捞一份,称作 cache miss;
    • 将读取到的数据塞入到缓存中,下次读取时,就可以直接命中。

    再来看一下写请求,规则是“先更新 db,再删除缓存”,详细步骤如下:

    • 将变更写入到数据库中;
    • 删除缓存里对应的数据。

    大厂高并发,“后删缓存”依旧不一致

    这种情况不存在并发问题么?不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

    1. 缓存刚好失效
    2. 请求A查询数据库,得一个旧值
    3. 请求B将新值写入数据库
    4. 请求B删除缓存
    5. 请求A将查到的旧值写入缓存

    如果发生上述情况,确实是会发生脏数据。

    然而,发生这种情况的概率又有多少呢?
    发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的,因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。

    一般情况下,读取操作都是比写入操作快的,但我们要考虑两种极端情况:

    - 一种是这个读取操作 A,发生在更新操作 B 的尾部。(比如写操作执行1s,读操作耗时100ms,读操作在写操作执行到800ms的时候开始执行,在写操作执行到900ms的时候结束,所以实际上读操作仅仅比写操作快了100ms而已)

    • 一种是操作 A 的这个 Redis 的操作时长,耗费了非常多的时间。比如,这个节点正好发生了 STW。(Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾回收器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起)

    那么很容易地,读操作 A 的结束时间就超过了操作 B 删除的动作。

    这种场景的出现,不仅需要缓存失效且读写并发执行,而且还需要读请求查询数据库的执行早于写请求更新数据库,同时读请求的执行完成晚于写请求。这种不一致场景产生的条件非常严格,一般业务是达不到这个量级的,所以一般公司不去处理这种情况,但高并发业务就非常常见了。

    image.png

    那如果是读写分离的场景下呢?如果按照如下所述的执行序列,一样会出问题:

    1. 请求A更新主库
    2. 请求A删除缓存
    3. 请求B查询缓存,没有命中,查询从库得到旧值
    4. 从库同步完毕
    5. 请求B将旧值写入缓存

    如果数据库主从同步比较慢的话,同样会出现数据不一致的问题。事实上就是如此,毕竟我们操作的是两个系统,在高并发的场景下,我们很难去保证多个请求之间的执行顺序,或者就算做到了,也可能会在性能上付出极大的代价。

    加锁?

    可以采用加锁在写请求中保证“更新数据库&删除缓存”的串行执行为原子性操作(同理也可对读请求中缓存的更新加锁)。加锁势必会导致吞吐量的下降,故采取加锁的方案应该对性能的损耗有所预期。

    image.png

    如何解决高并发的不一致问题?

    大家看上面这种不一致情况发生的场景,归根结底还是“删除操作”发生在“更新操作”之前了。

    延时双删

    假如我有一种机制,能够确保删除动作一定被执行,那就可以解决问题,起码能缩小数据不一致的时间窗口。

    常用的方法就是延时双删,依然是先更新再删除,唯一不同的是:我们把这个删除动作,在不久之后再执行一次,比如 5 秒之后。

    public void putValue(key,value){
        putToDB(key,value);
        deleteFromRedis(key);
        // 数秒后重新执行删除操作
        deleteFromRedis(key,5);
    }
    

    这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

    这种方案还算可以,只有休眠那一会,可能有脏数据,一般业务也会接受的。

    其实在讨论最后一个方案时,我们没有考虑操作数据库或者操作缓存可能失败的情况,而这种情况也是客观存在的。

    那么在这里我们简单讨论下,首先是如果更新数据库失败了,其实没有太大关系,因为此时数据库和缓存中都还是老数据,不存在不一致的问题。假设删除缓存失败了呢?此时确实会存在数据不一致的情况。除了设置缓存过期时间这种兜底方案之外,如果我们希望尽可能保证缓存可以被及时删除,那么我们必须要考虑对删除操作进行重试。

    删除缓存重试机制

    你当然可以直接在代码中对删除操作进行重试,但是要知道如果是网络原因导致的失败,立刻进行重试操作很可能也是失败的,因此在每次重试之间你可能需要等待一段时间,比如几百毫秒甚至是秒级等待。为了不影响主流程的正常运行,你可能会将这个事情交给一个异步线程来执行。

    而删除动作也有多种选择:

    • 如果开线程去执行,会有随着 JVM 进程的死亡,丢失更新的风险;
    • 如果放在 MQ 中,会增加编码的复杂性。

    所以到了这个时候,并没有一个能够行走天下的解决方案。我们得综合评价很多因素去做设计,比如团队的水平、工期、不一致的忍受程度等。

    异步优化方式:消息队列

    1. 写请求更新数据库
    2. 缓存因为某些原因,删除失败
    3. 把删除失败的key放到消息队列
    4. 消费消息队列的消息,获取要删除的key
    5. 重试删除缓存操作

    image.png

    异步优化方式:基于订阅binlog的同步机制

    那如果是读写分离场景呢?我们知道数据库(以Mysql为例)主从之间的数据同步是通过binlog同步来实现的,因此这里可以考虑订阅binlog(可以使用canal之类的中间件实现),提取出要删除的缓存项,然后作为消息写入消息队列,然后再由消费端进行慢慢的消费和重试。

    image.png

    1. 更新数据库数据
    2. 数据库会将操作信息写入binlog日志当中
    3. 订阅程序提取出所需要的数据以及key
    4. 另起一段非业务代码,获得该信息
    5. 尝试删除缓存操作,发现删除失败
    6. 将这些信息发送至消息队列
    7. 重新从消息队列中获得该数据,重试操作。

    小结

    针对 Redis 的缓存一致性问题,我们聊了很多。可以看到,无论你怎么做,一致性问题总是存在,只是几率慢慢变小了。

    随着对不一致问题的忍受程度越来越低、并发量越来越高,我们所采用的方案也越来越极端。一般情况下,到了延时双删这一步,就证明你的并发量已经够大了;再往下走,无不是对高可用、成本、一致性的权衡,进入到了特事特办的场景,甚至要考虑基础设施,关于这些每个公司的策略都是不一样的。

    除了 Cache-Aside Pattern,一致性常见的还有 Read-Through、Write-Through、Write-Behind 等模式,它们都有自己的应用场景,你可以再深入了解一下。

    题的忍受程度越来越低、并发量越来越高,我们所采用的方案也越来越极端。一般情况下,到了延时双删这一步,就证明你的并发量已经够大了;再往下走,无不是对高可用、成本、一致性的权衡,进入到了特事特办的场景,甚至要考虑基础设施,关于这些每个公司的策略都是不一样的。

    除了 Cache-Aside Pattern,一致性常见的还有 Read-Through、Write-Through、Write-Behind 等模式,它们都有自己的应用场景,你可以再深入了解一下。

    如果觉得不错,记得收藏+点赞 哦~

    展开全文
  • 目录一、缓存双写一致性场景1、先更新数据库,再更新缓存2、先更新数据库,再删除缓存2.1、存在的问题一:删除缓存失败,导致Redis和mysql的数据不一致。2.2、存在的问题二:删除缓存成功,但mysql主从时延问题,...
  • (1)先删缓存,再更新数据库 (不推荐)该方案会导致不一致的原因是 (2)先更新数据库,再删缓存(推荐)首先,先说一下 (2)缓存因为种种问题删除失败(3)将需
  • 分布式之数据库和缓存双写一致性方案解析.docx
  • 数据库与缓存双写一致性问题普遍出现在更新操作中。 先更新数据库,再更新缓存 一般来说,不这么推荐。原因是有些缓存数据是多张数据表联合计算出来的,那么就要求在每次更新以后就要同步计算出新的值,然后更新...
  • 高并发下如何保证数据库和缓存双写一致性

    千次阅读 多人点赞 2022-04-01 21:18:04
    今天这篇文章我会从浅入深,跟大家一起聊聊,数据库和缓存双写数据一致性问题常见的解决方案,这些方案中可能存在的坑,以及最优方案是什么。 1. 常见方案 通常情况下,我们使用缓存的主要目的是为了提升查询的性能...
  • 缓存双写一致性方案

    2020-04-13 15:39:29
    但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存。又或者是先删除缓存,再更新数据库,其实大家存在很大的争议。目前没有一篇全面的博客,对这几种方案进行解析。于是博主战战兢兢,顶着被大家...
  • 解决缓存和数据库双写数据一致性问题缓存的作用缓存和数据库双写不一致的原因并发引发的一致性问题先更新数据库,后更新缓存先删除缓存,后更新数据库先更新数据库,后删除缓存如何保证「第二步操作失败」的双写一致...
  • 今天这篇文章我会从浅入深,跟大家一起聊聊,数据库和缓存双写数据一致性问题常见的解决方案,这些方案中可能存在的坑,以及最优方案是什么。 1. 常见方案 通常情况下,我们使用缓存的主要目的是为了提升查询的...
  • 一、为什么这篇文章? 首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作: 但是在更新缓存方面,对于更新完数据库,是更新缓存...
  • 总结:强一致性缓存,这个本身就是伪命题。。用了缓存,只能保证最终一致性一定有可能在一段时间内,缓存是不新鲜的,哪怕只是几百毫秒。像下订单这种要求不能出错的业务场景,如果真的超大并发,只能是通过增加...
  • 分布式之数据库和缓存双写一致性方案解析!

    万次阅读 热门讨论 2018-05-21 10:55:36
    一、为什么这篇文章? 首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作: 但是在更新缓存方面,对于更新完数据库,是更新缓存...
  • 缓存双写一致性一. 何谓双写一致性二. 解决方案2.1 先更新数据库,再更新缓存2.1.1 问题1 每秒一万次改请求,一次读请求?2.2 先删除缓存,再更新数据库2.3 先更新数据库,再删除缓存2.4 最终问题制作不易,转载都请...
  • Redis缓存 什么是缓存 缓存就是数据交换的缓冲区,时临时存储数据的地方,一般读写性能比较高。比如CPU内部就有cache缓存,由于CPU运算的速度非常快,已经远远超过了内存的读写速度,以至于内存的读写速度降低了整体...
  • 本博文参考网上相关的博文,详细的整理下缓存数据库数据一致性的问题,并且给出基于Java的代码解决方案 关于缓存数据库数据一致性的解决方案,网上有很多,但是大都是偏向理论的,且大多数使用分布式锁来实现的,...
  • 2.1 延时删策略 2.2 异步更新缓存(基于订阅binlog的同步机制) 3 、基于binlog订阅实现步骤 3.1 准备材料 3.2 代码实现 1、冤孽的诞生 1.1 需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发...
  • 如何保证缓存和数据库的双写一致性 1、最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。 (1)、读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应 (2)、更新的时候先...
  • 个程序,只要使用了缓存 Redis 之类的就会面临双写一致性问题。很多程序员都会栽倒在这个问题上。 因为,不管你怎么回答,都看起来不是很完美。 首先,我们面临的是,你是先写缓存还是先数据库。假设,我们是先...
  • 在使用缓存时,我们必须要考虑的是缓存与数据库的双写一致性,是先删缓存还是先更新数据库?是需要强一致性还是最终一致性?延迟删策略真的就万无一失了吗?虽然网上已经有很多文章分析了,但都比较零散,所以本篇...
  • 双写一致性要求: 缓存不能读到脏数据 缓存可能会读到过期数据,但要在可容忍时间内实现最终一致 这个可容忍时间尽可能的小 一.先更新数据库,再更新缓存 线程A更新数据库,线程A更新缓存 线程B更新数据库,线程B...
  • 数据库缓存双写一致性的一些个人想法 有这么个问题: 说我们有个数据库,他的读请求特别多,以至于要在数据库上加一层缓存来抗压,这个都能理解吧。 这里的缓存,可能是和数据库一样的数据,也可能是数据库的数据...
  • 只要用缓存,就可能会涉及到缓存与数据库存储双写,你只要是双写,就一定会有数据一致性的问题 那么,如何解决一致性问题? 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统...
  • 在互联网领域,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没什么疑问,都是按照下...于是笔者战战兢兢,顶着被大家吐槽的风险,了这篇文章,如有不妥之处敬请在留言区指出,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 105,240
精华内容 42,096
关键字:

缓存双写一致性