偶然看到一篇介绍缓存和数据库一致性文章,点进去发现写的很细,配图也很多,但是读起来过于冗长,逻辑跳跃,很难给读者留下很清晰的印象。于是又搜了其他的一些相关文章,发现内容都差不太多,颇为失望。
因为本人一直以来追求清晰、明朗
,所以忍不住分享一下自己的一些想法,但水平有限,若有错误,还望不吝赐教。
比较懒,以下是纯文字,所以尽量简洁
首先,我们这里讨论的缓存一致性,指的是缓存和数据库的一致性(Cache & DB Consistency
),并非CPU缓存一致性。
对于缓存和数据库,就目前的技术而言,无法保证强一致性
,只能做到最终一致性
。所以我们的目标是尽可能地做到一致,但底线是最终一致性
。
注意:以下的讨论都是基于
Redis
(集群)和MySQL
(主从读写分离),讨论的点在于,线程在更新数据库时,对缓存应当作何操作。
我们假设数据更新前旧数据已经存入了缓存,由于底线是最终一致性
,所以在数据库更新之后
,对缓存一定要有一个操作,至于这个操作是什么,我们接着看。
>>> 数据库更新后的操作
我们只要找出这些操作可能出险问题的场景,就可以对其进行推翻。
> 1. 缓存即时更新(不可取)
问题场景:
- 线程A执行DB更新
- 线程B执行DB更新
- 线程B执行缓存更新
- 线程A执行缓存更新
数据库最终结果是B,而缓存里留下的是A,所以即时更新操作不可取。
> 2. 缓存延时更新(不可取)
问题场景:
- 线程A执行DB更新,睡眠
- 线程B执行DB更新,睡眠
- 线程B执行缓存更新
- 线程A执行缓存更新
数据库最终结果是B,而缓存里留下的是A,所以延时更新操作不可取。
> 3. 缓存即时删除(不可取)
问题场景:
- 线程A执行DB更新
- 线程A执行缓存删除
- DB从节点更新A线程数据
- (DB主从之间网络波动)
- 线程B执行DB更新
- 线程B执行缓存删除
- 线程C读取缓存失败
- 线程C读取数据库从节点数据
- 线程C写入缓存数据
- DB从节点更新B线程数据
数据库最终结果是B,但缓存被设置为了A,所以即使删除不可取。
> 4. 缓存延时删除(可取)
将缓存删除操作延时几百毫秒后进行操作,可以有效防止上述的DB主从之间的常见同步问题(暂不讨论主从同步长时间失败的情况)。
有人会选择将此删除任务丢到线程池中,线程先睡一会再去执行,这个操作效率低,不推荐。可以考虑使用线程池里的延时队列(DelayedWorkQueue
)。
考虑完了数据库更新完成之后的操作,现在来看,数据库更新之前的操作
>>> 数据库更新前的操作
其实这里的操作,都不会很完美,所以需要看业务的容忍度(Tolerance
)
> 1. 缓存更新(个人不推荐)
个人不推荐,因为大多数业务场景都比较注重,落库为安,一个数据还没有成功写入,它就有可能是问题数据。宁愿向用户展示旧的正常数据,也不要让问题数据出现。
> 2. 不作操作(看情况选择)
如果你的业务场景对于旧数据的容忍度比较高,可以选择这种方式,虽然正确的缓存会在数据库更新后的几百毫秒之后才能更新(配合上述的延时删除策略),但是你可以选择不在乎。
毕竟,可以省下一次缓存删除和一次缓存写入的开销,对缓存比较友好。
> 3. 缓存删除(推荐)
它和不作操作相比,多一次缓存删除和一次缓存写入的开销。(绝大多数情况下,不在乎这样的开销)
在理想的情况下,缓存删除后,下次查询缓存的线程,会在数据库更新完之后进行调用,这是数据没有问题。(在并发量不大的情况下,容易达成)
但如果查询调用的比较频繁,那么容易出现,在数据库更新的事务提交之前,就进行了缓存的查询,数据库查询操作,然后缓存会写入旧数据,且在几百毫秒之后才会更新为新数据(同样配合上述的延时删除策略)。
所以它并不完美,但比较下来性价比不错
,推荐。
>>> 小结
通过以上的阐述,基本可以确定,数据库更新前进行缓存删除,更新完成后进行延时删除是理智的选择,这其实也就是我们常说的延时双删
。
延伸:借助其他中间件
上面我们提到了,数据库更新后不能用更新缓存的方式,也不能用即时删除。
但是在借助Canal
中间件(由阿里开源,原理是监听数据库主节点的binlog
)以后,可以打破这些说法。
但代价是,项目复杂度大大增加。
至于它的具体用法,这里不作阐述,资料很多,可以自行查阅。