缓存一致性

2019/10/07 posted in  缓存

介绍

常见的系统开发中,经常会用到缓存,常见的查询结构如下图所示:

34A42FD7-471D-449A-8AA7-7962D6060235

但用到缓存,一定会涉及到缓存和DB之间数据一致性的问题。 当然,如果系统本身不要求一致性,那就可以忽略了,但大部分系统应该需要关注一致性的。只是对一致性的要求有高和低之分。

方案

常见的保证缓存一致性的方案有以下几种,各有优缺点,可以参考下。

方案1、先更新缓存,缓存更新成功后,再更新DB。

如果缓存更新失败,那DB也就不更新了,直接返回失败。

但如果更新缓存成功,但是更新DB失败,那就会造成数据不一致了。 所以这种方式存在问题。

方案2、先删除缓存,再更新DB

2D5EDF4F-3EA0-4823-B25E-D607497E0DE1

这是很多人推荐的,但其实并不完全适用,仍存在并发的数据不一致的情况。

在并发的情况下,假设我们缓存的是用户等级, value=1

顺序 T1请求 T2请求
1 DEL Cache
2 GET Cache Miss
3 Get DB value=1
4 SET Cache value=1
5 Update DB value=2

所以,原本更新DB之后,数据库中数据应该是2了,但因为并发T2的请求的原因,导致缓存中依旧是1.也就存在了缓存数据不一致的情况。

所以有人在此基础上,提出了一种 延时删除的策略。

先删除缓存,然后更新DB,更新成功之后,延时1秒之后,再删除缓存。 这样可以解决掉在这1秒中间带来的缓存数据不一致的情况。具体延迟1秒还是2秒,根据具体业务场景自行决定。

延时删除可以做成异步的,也可以做成同步的,根据业务的吞吐量要求自行取舍。

A16D905D-3D6E-44BD-976A-26C52C7C5236

方案3、先更新DB,再淘汰缓存。

80DCCB97-E3AB-43A8-A64F-F3009A29719A

从2的方案来看,其实第一步的淘汰缓存是没什么必要的。因为淘汰掉之后,依旧会存在并发情况下导致的缓存数据不一致的情况。

所以干脆就不删除缓存了,而是先更新DB,更新成功之后,再将缓存淘汰掉,这样,就可以保证在DB更新之后,再读取的数据,一定是最新的了。

这也就是老外提出的 Cache Aside Pattern 的思路。

但这种,也没法完全保证缓存数据的一致性。

同样以2方案中的例子为例

顺序 T1请求 T2请求 T3请求
1 Update DB value=2
2 DEL Cache
3 Get Cache Miss
4 Get DB value = 2
Update DB value=3
Del Cache
SET Cache value =2

所以,理论上Cache中应该是 3 ,但是现在却成了2. 存在了缓存数据不一致的情况。

但这种情况理论上发生的概率很小。 更新DB的时间理论上会大于读取DB的时间,读写缓存的时间也会很短。所以,理论上,在T3请求里面,很难出现读取完DB设置Cache的中间完成了更新DB和删缓存两个操作的情况。

所以,如果业务上可以容忍这种的缓存不一致的话,是可以用这种方案的。

如果要求较高,可以利用方案2的延时双删的策略,延时5s后在删除一次。 (比如读操作耗时大于写操作,举例:缓存中存储的是一个列表,更新一条数据的时候,列表的缓存也要删除掉,重新加载列表时,耗时远大于更新的耗时,这时候就容易出现上面的问题)

但其实这样,依旧可能存在至多5秒的缓存数据不一致的情况。如果业务要求严格一致,可以在这5s内,不读取缓存,而是直接读取DB。 (感谢鹏哥指点)

大概流程如下:

写操作:

1、操作DB

2、删除缓存

3、设置缓存标志位,比如 key1 ,过期时间3s

4、返回数据给调用方

读操作:

1、读缓存key1值是否存在

2、key1存在,则读取DB数据,写回缓存,数据返回业务方。(表明当前数据刚更新,需要读取DB获得准确数据)

3、key1不存在,读取缓存是否存在

4、缓存存在,则直接返回。

5、缓存不存在,则查询DB,并将数据写回缓存,数据返回业务方。

前面这2种方案(删缓存—>更新DB—>延时双删 和 更新DB—>删除缓存)都避免不了的一个问题是,在更新DB之后,可能不一定删除缓存成功,所以最终还是会存在数据不一致的情况。这种情况下,一般可以选择失败重试的办法来降低这种失败的概率。基本业务可以满足。

另外,缓存使用的时候,增加缓存过期时间,一定程度上也能缓解缓存不一致的情况,算是个兜底方案。

注意:第一种和第二三种是不一样的,缓存更新成功,不一定意味着DB会更新成功,比如业务上DB上一定不能更新成功(EG:主键冲突,唯一索引冲突等),这种情况下,即使失败重试,也不能解决,所以,第一种方案不推荐使用。

方案4: 更新DB,成功后,更新缓存。(存在问题,不推荐使用)

F2732811-FC70-44A8-9127-7A329C70CA1E

这种很容易造成缓存不一致的情况。同样,以前面的例子为例,原始数据Value=1

顺序 T1请求 T2请求
1 Update DB value=2
2 Update DB value = 3
3 Set Cache value = 3
4 SET Cache value=2

最终, DB中是3,但是缓存中却是2. 存在了缓存不一致。 这种情况的发生概率远大于方案3中的概率,因为都是更新操作,这里面的时间差就在于更新DB和写缓存之间的时间,这种就比较容易发生了。

方案5:更新缓存—>异步写DB

这种方案和方案1 很像,但这种方案的前提是,数据全以缓存为准。DB为辅。

如果缓存中存在数据,则直接返回。 如果缓存中不存在,则从DB中获取,并写入缓存。 更新时,更新缓存成功后,则直接返回,异步更新DB直到更新成功。有点类似文件系统的Page Cache的思路。 这种的速度会特别快,毕竟同步的只有内存的IO。数据库的IO全都异步了。适合DB无业务逻辑的情况下,比如不存在触发器,不存在唯一索引等,只要保证缓存更新成功之后,DB在业务上一定可以更新成功即可。 对性能要求很高的,可以考虑这种方案。

关于我及张二蛋又要扯蛋了

    一个不务正业的程序猿及这个程序猿写字的地方,这里可能有技术,有理财,有历史,有总结,有生活,偶尔也扯扯蛋,妥妥的杂货铺,喜欢可关注。
    酒已备好,等你来开
图片