推荐你阅读
互联网大厂万字专题总结
Redis总结
JUC总结
操作系统总结
JVM总结
互联网大厂常考知识点
什么是系统调用
CPU底层锁指令有哪些
AQS与ReentrantLock原理
旁路策略缓存一致性
二级缓存
二级缓存是一种最简单的缓存模型,它在各个地方都有所应用,比如 CPU 核心的 L1、L2 Cache 与内存构成的 CPU Cache 缓存,一个 CPU Cache Line 缓存一个 64 字节大小的内存块,采用写直达/写回策略,再比如内核内存和磁盘文件构成的 Page Cache 缓存,内核内存一个文件页缓存磁盘文件的一个大小为 4KB 的数据块,采用旁路策略/写回策略,并使用 LRU 算法淘汰内存中的文件页和匿名页,再比如最常见的 redis 加 mysql 的二级缓存,在读多写少的情况下常采用旁路策略。
任何缓存机制,所需要考虑的无非是数据一致性问题和性能问题的权衡。对于二级缓存,采用分布式锁将保证所有增删改查操作的原子性将完全丢失性能,因此不会采用这种方式
旁路策略
增
在数据库中增加相应条目
删
先删除数据库中相应条目,再删除缓存中相应条目
改
先修改数据库中相应条目,再删除缓存中相应条目
查
先查缓存,如果有数据就直接返回,如果没有数据就查数据库,如果没有数据就返回空,有数据的话存入缓存后返回
并发一致性分析
那么为什么旁路策略的增删改查方式几乎不会出现数据一致性问题呢?首先需要明确以下几点
- 数据库保证了关于相同条目读写的原子性
- 缓存保证了关于相同条目读写的原子性
- 前后两个操作之间存在顺序性,比如改操作,先改数据库再删缓存
- 数据库的读写时间远远长于缓存的读写时间
下面是四种操作的时序图,其中查询只考虑了缓存没查到的情况,因为缓存查到了就会直接返回,并不会带来数据一致性问题
首先明确如下两种情况从结果上来看是等价的,比如我们把改和查 2 放在一起,其中改操作的第一步修改数据库操作完成后,根据顺序性,当前线程下一步的删除缓存开始执行,根据原子性,另一个线程的查询数据库开始执行。由于数据库的读写时间远远长于缓存的读写时间,所以可以等价于先执行删除缓存,再执行查询数据库,这在结果上是一样的。即结果等价于改和查 2 是串行执行的。
接下来考虑增、删、改、查 1、查 2 这几种操作的任意并发情况,其实从结果上来看等价于所有操作都是串行执行的,因此不会出现数据库和缓存之间的数据一致性问题。
一些额外的操作
上面我们已经证明了旁路策略的增删改查操作不会带来并发数据一致性问题,然而有时我们会有一些额外的操作,比如下面这些操作也是不会带来数据一致性问题的。
手动缓存失效或超时缓存失效
比如我们设置一个接口,让相应缓存条目失效,或者设置缓存本身的超时过期。
读缓存
比如我们设置一个接口,读缓存中的相应条目,或查操作本身第一步就是读缓存。
为什么先改再删而不是先删再改
之所以先改再删是为了将耗时长的数据库读写操作放在前面,将耗时短的缓存读写操作放在后面,这样才能保证串行等价性。
而如果先删再改的话,就有可能出现一方包裹另一方或者交替执行的非串行执行方式,如下图所示
先删再改并发问题实例
下面这种情况很容易发生
先改再删并发问题实例
下面这种情况几乎不可能发生
改操作是删除缓存还是更新缓存
改操作可以修改数据库然后删除缓存,也可以修改数据库然后修改缓存,选择哪种方式主要根据场景而定,如果缓存可用空间比较小,建议采用删除缓存的方式及时清理缓存,如果缓存可用空间比较大并且也配置了缓存过期时间,则建议采用修改缓存的方式。
超时缓存过期的兜底方案
无论任何时候,我们都不保证二级缓存百分百的一致性,先改再删的小概率事件、程序异常等原因都可能造成二级缓存的数据一致性问题,而既然采用了缓存就说明业务并不是要求时刻强一致性的(比如金融业务),因此小概率的短期缓存数据不一致是可以接受的,因此我们需要设定策略在出现缓存不一致的情况下尽快恢复一致性。可以采用的方式包括定期清除缓存,或者设置超时缓存过期来兜底,这样即使出现了数据不一致的情况,也能在较短时间后恢复一致。
删除缓存失败的解决办法
比如在数据库和缓存都有值的情况下进行改操作,先改数据库成功了,然后删除缓存失败了,此时缓存中就一直是旧值了。
解决方法有两种
- 重试机制
- 订阅 MySQL binlog,再操作缓存
重试机制
本质是当删除缓存失败的时候,不断重试直到删除成功,因为删除缓存失败本来就是小概率事件,所以几次重试的成功概率可以认为达到百分百。
可以利用消息队列的消费确认功能完成,修改数据库后生产一条消息到 mq 中,由消费者消费这条消息并删除缓存,消费失败就重新获取消息并重试,直到消费成功确认。