随笔分类
缓存与数据库的双写一致性:
读时:先读缓存,未命中再去读数据库
写时:对缓存与数据库的写更新顺序值得探讨,其实无非四种:① 先写缓存,再写数据库 ② 先写数据库,再写缓存 ③ 先删缓存,在写数据库 ④ 先写数据库,再删缓存
先写缓存,再写数据库
思考一种情况,缓存更新成功,此时发生了网络中断异常,未能去更新数据库,也就是说,缓存此刻存储的是最新数据,数据库里头存储的是旧数据,二者数据不一致,即缓存此时存储的是脏数据 (缓存是基于内存的分布式存储,若缓存宕机或后续关闭缓存后,req直接命中的会是 db,此时便发生数据不一致),这种影响危害较大
先写数据库,再写缓存
思考一种情况:数据库更新成功,此时网络发生了中断异常 (或网络卡顿),未能去更新缓存,即缓存此时存储的是旧的数据,二者数据不一致
并且在高并发场景下,多线程若同时去操作相同的数据,可能会存在缓存中数据被旧数据覆盖的场景
先来思考:写缓存失败,该如何去做?在并发量不高的场景,可以使用事务
来保证数据更新的一致性,假若缓存更新失败了,可以对数据库中数据进行回滚复原,但并发量上来后不建议这么去玩,无论是更新数据库,还是更新缓存,实际上都是远程操作,对应的便是 长事务
,而出于死锁的避免,通常是建议不要把更新缓存、数据库的 ops放到同一个事务中去
而高并发下,覆盖的场景又是怎样的?如图:
可以看到,b操作后执行,理论上来说缓存中存储的应该是最新值,但由于 a操作过程中由于网络抖动等原因,导致缓存中存储的值旧的数据,这在高并发场景中较为常见
还有一个问题便是,这种双写实际上是比较消耗系统资源的:每一次写操作,都要去更新缓存,一方面,若写的缓存不是简单的数据内容,而是通过较为复杂的计算而最终计算出来的结果,这实际上是比较消耗内存以及 cpu资源的,有点得不偿失;其次,此次更新缓存后,并不能保证缓存中内容在下一时刻得以访问
由此,多数情况下,我们不会选择去写缓存,而是删除缓存,并由下一次访问的未命中来实现缓存的懒刷新
先删缓存,再写数据库
但这种方式在高并发场景下,有可能出现 db还未来得及更新,又去将缓存设置为了旧值,如图:
虽然出现的数据不一致的场景,但我们发现,若再写数据库之后,再去删缓存,不就能解决问题了吗?这实际上就是 缓存双删
的方式,并且出于保证删除缓存的正确性,通常是间隔一段时间后再去删除缓存
但这有会引发新的问题,假若第二次缓存删除失败了怎么办?
这我们后面接着讨论
先写数据库,再删缓存
上述方案其实都有存在长期的数据不一致的可能,so我们寄希望于最后一种方案了
思考两种 case:
-
a写请求写完数据库后,网络发生抖动,还未来得及删除缓存,此时 b读请求来了
显而易见,b读请求直接命中了缓存,获取旧数据返回
但 a请求网络恢复后,缓存会被删除,之后再来新的读请求缓存中被刷新,存储最新的值
这种 case下是可以容忍下,缓存中最终存储的也只会是最新数据
-
a读请求先过来
读请求命中缓存,返回
b写请求写数据库,在删缓存
对于上述第一种 case,假设其再去查询缓存之前,缓存失效
了,并且在查询数据库(旧值)之后,网络也发生了抖动,还未来得及更新缓存,此时来了新的写请求,写数据库(新值),删除缓存,此时原先读请求网络恢复,去将缓存更新为了旧值,此时便发生了数据的不一致
这种实际上是小概率事件:
- 缓存刚好失效
- 读请求查询数据库获取旧值,更新缓存的耗时长于其它写请求写数据库,删除缓存的耗时
多数情况下,读多于写,并且读快于写
实际开发上,推荐先写数据库,再删缓存,虽然并不能 100%保证数据一致性,但相较于其它方案而言是最为稳妥的
删除缓存失败了怎么办?
选用失败重试机制
!
在初次删除缓存失败后,立马进行一定次数的重试机制,并且出于同步操作耗时影响,重试次数有限,若达到上线次数还未成功删除缓存,可以去数据进行落盘,开启定时任务异步执行缓存的删除,亦或是采取 mq机制,将删除缓存的 action作为一种消息存放到队列中,订阅者异步轮询队列消费元素