昨天梦塔给大家分享了《一文学会如何使用缓存(上)》,我们对缓存做了一些整体上的概述,那今天呢,我们讲下如何正确使用缓存!
01
使用缓存会遇到哪些问题
虽然缓存可以提高整体性能,但是它也可能会带来别的问题。
例如使用缓存之后,就相当于把数据存放了2份,一份是在数据库中,另一份存放在缓存中。
当有新的数据要写入或者旧数据需要更新的时候,如果我们只更新了其中一份数据源,那两边的数据就不一致了,所以这里就存在一个缓存数据与数据库数据如何进行有效且快速的同步问题,才可以保证数据的最终一致性。
另外,加上缓存服务其实也引入了系统架构的复杂度,还需要额外的关注缓存自身带来的下列问题:
缓存的过期问题
设计缓存的过期时间需要非常的有技巧,且必须与业务实际情况相结合。
因为如果设计的过期时间太短了,那会导致缓存效果不佳,且还会造成频繁的从数据库中往缓存里写数据。
如果缓存设计的过期时间太长了,又会导致内存的浪费,这里需要根据业务把控一个度。
缓存的命中率问题
这也是设计缓存中需要存放哪些数据的很重要一点,如果设计的不好,可能会导致缓存命中率过低,失去缓存效果。
一般对于热点数据而言,要保证命中率达到70%以上效果最佳,这里就要求并不是所有的业务都适合使用缓存。
2
缓存一致性问题
缓存由于其适应高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3bypu2eN-1648380574995)(https://upload-images.jianshu.io/upload_images/27042338-875955f60c3cc68a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存,又或者是先删除缓存,再更新数据库,其实存在很大的争议。
被动失效
被动失效是给缓存设置过期时间,是保证最终一致性的解决方案。
在这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可,也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
但是这种方案在更新数据后需要等一段时间才能更新成最新值,时效性不高,并且依赖于过期时间,如果设置为永不过期,则可能缓存永远得不到更新。
主动更新
相对于被动失效,主动更新是大部分场景使用的方案,常见的更新方案有以下几种:
先更新数据库,再更新缓存
先删除缓存,再更新数据库
先更新数据库,再删除缓存
先更新数据库,再更新缓存
这套方案,一般是不被大家所接收的,主要有以下两方面原因
线程安全角度:
同时有请求A和请求B进行更新操作,那么有可能出现如下的场景
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此这一套方案被废弃掉不予考虑。
业务场景角度:
如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存,那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。
先删缓存,再更新数据库
我们会基于这个方案去实现缓存更新,但是不代表这个方案在并发情况下没问题
数据不一致
同时有一个请求A进行更新操作,另一个请求B进行查询操作
上述情况就会导致不一致的情形出现,而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
如何保证一致性
没有办法做到绝对的一致性,这是由CAP理论决定的,缓存系统适用的场景就是非强一致性的场景,所以,我们得委曲求全,可以去做到BASE理论中说的最终一致性。
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。在此我向大家推荐一个架构学习交流圈。交流学习伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
达到最终一致性的解决思路,主要是针对先删缓存,再更新数据库/先更新数据库,再删缓存的策略导致的脏数据问题,进行相应的处理,来保证最终一致性。
延时双删
先删缓存,再更新数据库,然后等待一段时间,在删除缓存,等待数据完全落盘后删除缓存完成同步操作。
伪代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xkr1ki0f-1648380574996)(https://upload-images.jianshu.io/upload_images/27042338-57a7b06aba4dc1ca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
如何确定休眠时间
延时双删为了保证第二次删除没有问题,如何确定延时时间呢?
针对不同的业务场景,应该自己跟根据项目评估出来一个靠谱的休眠时间,然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可,这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
读写分离
在读写分离的场景下延时时间还需要考虑主从同步的时间,如果不考虑将出现以下问题
上述情形,就是数据不一致的原因,还是使用双删延时策略,只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms
第二次删除失败怎么办
第二次删除可能因为各种原因导致失败,虽然概率不高但是还是可能出现的,还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库。
如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题,为了防止这种情况产生,可以采用下面的先更新数据库,在删除缓存的方案
先更新数据库,再删缓存****Cache-Aside pattern
这是国外提出的一个缓存更新的思路,名为《Cache-Aside pattern》
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
操作步骤
我们用三个线程A、B、C来模拟以下多线程下的操作
这钟情况下线程B有短暂的不一致情况的产生,但是下一次查询就会被正确的数据给修复
并发问题
先更新数据库在更新问题也存在一些一致性问题,但是概率很低,假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生。在此我向大家推荐一个架构学习交流圈。交流学习伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
这种情况下发生的概率很低,只有满足以下条件才会发生
步骤2读数据库操作要比步骤3写数据库操作时间要长,才有可能发生步骤4先于步骤5发生
但是实际上数据库的读操作的速度远快于写操作的,因此这种情况下只有很小的概率发生
如何解决并发问题
只要感觉可能发生,在以后很有可能发生,怎么避免呢?
可以采用设置缓存的失效时间来避免,因为概率很低,并且有失效时间限制,定时会进行数据的同步,可以在一定程度上避免上述问题的发生,但是还是还是有概率发生的
异步删除方案
上面的缓存同步方案都这样那样的问题,如何一揽子解决呢,可以考虑异步删除方案
方案架构
可以采用这种基于监控Binlog的同步方案,并且使用消息队列做一次缓存的异步删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ctzf0HD3-1648380574997)(https://upload-images.jianshu.io/upload_images/27042338-3f32898ad984ec3b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
具体流程图下
1.更新数据库数据
2.数据库会将操作信息写入binlog日志当中,并且同步到canal中
3.订阅程序会从canal中提取出所需要的数据以及key,并且进行过滤清洗
4.另起一段非业务代码,获得该信息
5.尝试删除缓存操作,发现删除失败
6.将这些信息发送至消息队列
7.重新从消息队列中获得该数据,重试操作
优点
**零入侵:**上面不管是先删缓存还是先更新数据库都需要对业务代码进行更改,但是这种方案可以在不用更改任何代码的基础上进行操作
**高可用:**如果删除失败,我们还可以基于MQ的重试机制进行保证一定会删除成功的
**顺序性:**不会出现并发问题,这里binlog同步必然是在数据库操作成功后才会触发后续的操作
缺点
缺点是复杂性高,需要引入canal以及MQ等中间件,维护成本高。
阿里面试面试题:链接:https://pan.baidu.com/s/1ryi6EMUEjZvlaRGEnipryw
提取码:tn3g