介绍
常见的系统开发中,经常会用到缓存,常见的查询结构如下图所示:
但用到缓存,一定会涉及到缓存和DB之间数据一致性的问题。 当然,如果系统本身不要求一致性,那就可以忽略了,但大部分系统应该需要关注一致性的。只是对一致性的要求有高和低之分。
方案
常见的保证缓存一致性的方案有以下几种,各有优缺点,可以参考下。
方案1、先更新缓存,缓存更新成功后,再更新DB。
如果缓存更新失败,那DB也就不更新了,直接返回失败。
但如果更新缓存成功,但是更新DB失败,那就会造成数据不一致了。 所以这种方式存在问题。
方案2、先删除缓存,再更新DB
这是很多人推荐的,但其实并不完全适用,仍存在并发的数据不一致的情况。
在并发的情况下,假设我们缓存的是用户等级, 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秒,根据具体业务场景自行决定。
延时删除可以做成异步的,也可以做成同步的,根据业务的吞吐量要求自行取舍。
方案3、先更新DB,再淘汰缓存。
从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,成功后,更新缓存。(存在问题,不推荐使用)
这种很容易造成缓存不一致的情况。同样,以前面的例子为例,原始数据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在业务上一定可以更新成功即可。 对性能要求很高的,可以考虑这种方案。