持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
背景
当数据更新后,缓存也需要跟着更新。不然数据库和缓存的数据就会不一致,那具体应该怎么处理呢?
关键字:缓存一致性,延迟双删
缓存一致性问题
缓存一致性问题有两种。一种是持久层和缓存层的一致性,一种是多级缓存之间的一致性,本篇博客围绕的是前者。
另外,这里的一致性是指的最终一致性。持久层和缓存层是两个服务,如果要保证强一致性的话,就要引入两阶段提交、三阶段提交、Raft等分布式协议,系统的复杂度会提升,服务本身的性能也会受到影响,也就抹杀了缓存带来的好处。所以这里一般不会追求强一致性。
目前,解决缓存一致性问题有四个方式:
- 先更新DB,后更新缓存
- 先更新缓存,后更新DB
- 先删除缓存,后更新DB
- 先更新DB,后删除缓存
我们看看日常工作中,应该使用哪种。
先更新 DB,后更新缓存
假设初始数据是 V0,现在并行发起了 A、B 两个线程,A 线程把数据更新成 V1,B 线程把数据更新成 V2。
因为是并行发起的,线程调度上如果出现下面这类情况:A 线程先执行,之后 B 线程抢占到了 CPU 先执行完。就会导致缓存不一致。
两个线程全部执行完后,DB中的数据是 V1,但是缓存中是 V2。
所以出于线程安全考虑,不推荐这种方式。
另外,出于性能考虑,也不推荐这种方式。因为更新操作比较耗时,可能辛辛苦苦更新完,发现根本没人访问,白白浪费了系统资源。
所以,这一方式不行,因为有线程安全问题,性能也不佳。
先更新缓存,后更新 DB
同样,假设初始数据是 V0,现在并行发起了 A、B 两个线程,A 线程把数据更新成 V1,B 线程把数据更新成 V2。
可以看到,这种方式,也会出现一样的问题。两个线程全部执行完后,DB 中的数据是 V1,缓存中的数据是 V2。
所以,这一方式也不行,因为有线程安全问题,性能也不佳。
先删除缓存,后更新 DB
我们再来看看先删除缓存,后更新 DB 的方式。
假设初始数据是 V0,假设现在有 A、B 两个线程,A 线程把数据更新成 V1,B 线程是要读取数据。
因为是并行发起的,线程调度上如果出现下面这类情况,就会导致缓存不一致。线程B把老的数据加载进缓存,缓存中的数据是 V0,DB是 V1。
另外,现在大家处于性能和高可用的考虑,都是主从架构,主从架构就会有主从延迟的问题。引入缓存后,会因为主从延迟导致不一致。
如图,线程 A、线程 B 执行完后DB中的数据是 V1,缓存中的数据却仍然是老数据 V0
所以,这一方式也不行,因为有线程安全问题,也可能会因为主从延迟导致不一致
先更新 DB,后删除缓存
排除三个错误答案后,就剩这一个了,这个应该可以了吧?不,极端情况下,也是会的出现不一致的。
我们假设初始数据是 V0,假设现在有 A、B 两个线程,A 线程把数据更新成 V1,B 线程是要读取数据。
t1 时刻缓存过期,这个时候 B 线程先发起读取,A线程紧跟着发起更新,如果出现下图所示的调度顺序,两个线程执行完后,缓存中的数据是 V0,但是DB中的是会是 V1。
之所以说是极端场景,是因为要同时满足以下两个条件: 1.缓存到期后,同时来了一个更新请求和读取请求(高并发场景下,这个条件是有概率满足的) 2.读取请求先发起,但执行的比写请求慢(写操作涉及到加锁、删缓存等操作,理论上是比读请求慢的) 尤其是第二个条件,考虑到锁、写逻辑本身的复杂性,要满足是很困难的。
然后,由于主从延迟,也会导致不一致:
所以,这一方式也不行,极端情况下会有不一致,也可能会因为主从延迟导致不一致。
这也不行,那也不行,光提出问题,不给解决方案,这分享了个什么玩意?!
不要关掉,还有还有!
延迟双删策略
先更新DB,后删除缓存出现缓存不一致的原因,在于删除缓存的操作没有起到作用,缓存仍然是老数据。那我等一会再删是不是就行了?这就是延迟双删的思路。步骤如图:
再考虑以下细节:
- 延迟删除操作,影响性能怎么办?我们可以异步的搞
- 延迟删除失败了怎么办?那就重试,重试还失败就报警,然后手动处理
- 延迟若干秒,那具体延迟多少呢?可以基于主从延迟的时间进行评估,防止主从延迟带来的不一致(极致一点的,可以监听binlog触发第二次的删除操作)
延迟双删策略本质其实是确保数据库更新后,缓存一定会被删除,减少了缓存不一致的时间。 两次删除的时间段内,仍然可能出现会有缓存不一致的问题。比如 t5 时刻,DB 已经更新成了 V1,缓存中的数据仍然是老的 V0。
已数据库 binlog 作为缓存更新的 trigger
延迟双删策略是圈子里比较流行的方式,但动手实现起来就会发现,实现成本是不小的,复杂性提高也会给后续的维护带来难度。
后来,我看见有的团队通过直接监听 binlog 来实现,实现成本比较低,对原有的代码也没有侵入,缓存命中率也会高一些,流程图如下。
这种方式实现成本低,但是缺点是会因为主从延迟导致缓存不一致,采用前一定要基于业务评估是否可以用这个方案。
直接走主库
上述两种优化方案优化的是什么?优化的是减少数据库主从延迟带来的影响,把缓存不一致的时间努力控制在主从延迟的时间之内。大多数业务其实都是能接受的。
那么,什么方案可以保证缓存一致性不受主从延迟影响呢?那就是走主库了。
当缓存到期,直接走主库更新缓存,是非常实用的一种解决缓存一致性的方式。
使用这种方式,我们要考虑一下问题:
- 接口隔离:如果对一致性要求不是很强的业务,也就没必要这么搞了,可以专门开一个简单的接口供他们使用,然后专门开一个一致性更强的接口给特定的业务方使用,这样可以减少主库的负载。
- 注意缓存穿透和缓存击穿问题 缓存系统设计:缓存雪崩、缓存穿透、缓存击穿
- 限流保护:最后一层保护伞,防止直接把数据库打卦
总结
解决缓存一致性问题的四个方式其实都有问题,好在有优化方案。日常工作中,我们要基于业务场景判定,具体使用哪种优化方案。
好,就酱,bye bye。下一篇文章分享 Cache Pattern,点个赞吧,求求你了! QAQ
引用
- 美团二面:Redis与MySQL双写一致性如何保证? juejin.cn/post/696453…
- 分布式之数据库和缓存双写一致性方案解析 www.cnblogs.com/rjzheng/p/9…
- 【原创】分布式之数据库和缓存双写一致性方案解析(二) www.cnblogs.com/rjzheng/p/9…
- 【原创】分布式之数据库和缓存双写一致性方案解析(三) www.cnblogs.com/rjzheng/p/9…
- 到底是先更新数据库还是先更新缓存? baijiahao.baidu.com/s?id=170576…