缓存与数据库的一致性问题怎么解?三招帮你搞定 - Go语言中文社区

缓存与数据库的一致性问题怎么解?三招帮你搞定


通常在高性能要求的场景,我们的系统设计会把数据存储到DB,然后冗余一份数据在缓存中。读请求优先从缓存读取数据,未命中缓存再从DB读取,如下图:

缓存与数据库的一致性问题怎么解?三招帮你搞定「程序员必备」

欢迎关注笔者,优质文章都在这里等你。

这样做的好处是可以减小DB的压力,提高请求的响应速度。

但这种架构在提升系统读请求处理能力的同时,给系统写请求的处理带来了不少的麻烦。因为数据在DB跟缓存中各自保存了一份,如何保证它们之间的数据一致就是本文要讨论的问题。

当处理写请求时有两种方式:

一、先写缓存再写DB

如果第一步写缓存失败,直接返回,无影响。

如果缓存写成功,DB写失败,此时如果不清除缓存中已写入的数据,则会造成数据不一致(缓存中是新值,DB中是旧值)。

如果增加清除缓存的逻辑,那么清除操作又失败了该如何处理?

二、先写DB再写缓存

如果DB写入失败,直接返回,无影响。

如果DB写入成功,缓存写入失败则会造成数据不一致(即DB中是新值,缓存中是旧值)。

如果重试写入缓存,那重试也失败该如何处理?

三、问题解决

1. 通过异步线程解决

就上面所说的场景来说,发生失败时,我们可以开启一个异步线程去做数据回填操作,反复重试直到成功。如果采用异步线程回填数据的方式做最终一致性,那么这个容错性是内存级别的,也就是说如果此时重启服务(线程消失),那么这个重试任务就丢失了,导致数据不一致。

2.通过定时任务解决

如果发生失败,可以写入一个异步服务,然后通过定时任务不断重试,回填数据,直到成功,保证数据的最终一致性,因为任务是可以持久化的,所以不用担心重启等问题。

3.通过状态校验解决

这也是本文主要介绍的一种比较巧妙的方式。

一、写请求流程:

缓存与数据库的一致性问题怎么解?三招帮你搞定「程序员必备」

如上图,每次处理写请求时,将会经过如下几个步骤:

  1. 首先针对要写入的数据设置一个状态,失败则结束,成功则转2。
  2. 如果设置状态成功,则直接清除缓存,失败则解除状态并结束,成功则转3。
  3. 清除缓存后,再写入DB,失败则解除状态并结束,成功则转4。
  4. DB写入成功以后,把新值回填缓存,失败则解除状态并结束,成功则转5。
  5. 回填成功,解除状态并结束。

二、读请求流程:

缓存与数据库的一致性问题怎么解?三招帮你搞定「程序员必备」

如上图,每次处理读请求时,将会经过如下几个步骤:

  1. 直接从缓存读取数据,成功则结束,失败则转2。
  2. 从DB读取数据,失败则返回,成功则转3。
  3. 根据从DB读取到的数据判断该数据对应的状态,如果没有状态,则回填缓存并结束,如果有状态,则直接结束。

总结来说就是我们通过一个状态把读写请求关联起来,这里先不讨论这个状态的实现细节以及各种容错,比如说解除失败以后怎么处理。

三、失败情况下数据一致性分析

接下来分析一下各个过程失败以后,读写请求是如何通过状态位解决数据不一致的问题的,如下:

  1. 获取状态失败,直接返回失败信息。
  2. 此时写请求知道自己写失败了并且缓存跟DB中也都是旧值,没有造成数据不一致问题。如果获取状态成功,只是网络问题造成失败,这种场景下写请求并没有对数据进行任何修改,因此不会导致数据不一致,只要在状态实现的方案中考虑到这个容错即可,细节后面再讨论。
  3. 清缓存失败,解除状态并返回失败信息。
  4. 如果清缓存失败,则缓存中还有旧值,没影响。如果清缓存成功,失败是因为网络造成的,即缓存中已经没有数据,此时缓存跟DB虽然数据不一致,但后面读请求会直接从DB读取数据,然后查询数据对应的状态,如果状态已经解除则回填缓存,如果状态还存在,也就是说在写请求清缓存成功(数据清理成功)到解除状态之间的读请求会直接返回DB中的值,这样所有的读请求读取到的值都是一样的,没有数据不一致问题。
  5. 写DB失败,解除状态并返回失败信息。
  6. 此时缓存已经没有值,如果读请求发生在清除缓存之后,写入DB之前,那么会从DB读到旧值,由于这次写请求还没完成,所以读到旧值是合理的,并且由于此时数据的状态还在,读请求并不会把该旧值回填缓存。如果DB写入失败,则DB中是旧值,后面的读请求会把该数据回填缓存。如果写入成功,但由于网络问题导致失败,此时DB中是新值,缓存中没有值,那么后续的读请求会读取到新值并回填缓存。但写数据的请求收到的却是写失败,这就造成了写请求失败,但读请求已经读取到了新值。也就是说写请求是成功了,但写请求返回的处理结果是失败,此时如果写请求忽略了这个错误什么也不做,那么可以认为写成功了,因为读取到了新值并且没有数据不一致的问题。如果写请求收到错误以后没有忽略而是进行了重试,那就要求写操作满足幂等性,在分布式系统中解决写操作时由于网络原因导致的失败问题必须通过幂等解决,这没有问题。
  7. 回填缓存失败,解除状态并返回。
  8. 此时如果回填成功,失败是由网络造成的,那么读请求直接走缓存读取,没有问题。如果回填失败,后面的读请求从DB读取到数据以后会根据状态进行回填缓存的操作,最终数据保持一致。这里其实回填操作不是强制要求的,也就是说如果DB写入成功,回填缓存失败,完全可以返回写入成功,写请求不需要重试,回填交给后面的读请求。

综上所述,这种读写方案在各种失败的场景中均满足了数据最终一致性。虽然过程中缓存跟DB会有数据不一致(缓存中没有值,DB中有值)但所有的读请求读取到的数据都是相同的。

参考文章:https://www.jianshu.com/p/a532962cb9e9

  • 发表于 2019-06-28 11:08:03
  • 阅读 ( 2579 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢