00:01
前面我们给大家说了一下spring catch的使用,我们使用了两个注解catch和catch evict,那接下来我们再来说一下spring catch的不足,因为在使用缓存场景的时候,那对缓存做了很多的分析,首先我们说毒模式,毒模式呢,缓存有很多的分险,比如我们说的缓存穿透,还有缓存我们这个击穿,还有我们这个缓存雪崩,那这几个问题,缓存穿透那指的是我们查询一个永不存在的数据,查询一个浪数据,缓存中没有我们一直去找数据库,我们将大量请求放过来,去来查数据库,这就是一个危险的操作。所以我们说解决方案是我们的缓存空数据,缓存空数据,而spring catch对这一块有没有解决呢?我们发现在这个配置文件中,我们就看到了叫catch呢,Values我们配了一个触,所以呢,这一块我们不用担心。
01:02
然后呢,还有一个叫缓存击穿,它指的呢,是大量咱们这个并发进来,同时查询一个正好过期的数据,同时查询一个正好过期的实际数据。由于我们这个缓存中没有,然后呢,我又把它们放给数据库,假设这有10万并发,那十万全放给数据库,这又会出现问题,怎么说这个解决方案是什么呢?它就是我们说的加锁,那只要像我们以前的方式,那在防止缓存击穿的时候,我们将所有的服务来都加上锁,最终想要查数据库了,来锁定让一个人去来查,下次的人再想进来了,那就应该他得到锁以后再来操作。所以我们说这个我们spring catch有没有解决呢?来一会儿研究一下,包括缓存雪崩,雪崩我们指的是大量的key同时过期,这个同时过期呢,其实在我们超大型系统里边可能会存在,但是在我们正常的应用里边,我们即使这些K同时过期,但是呢,只要这些K不是十几万个K同时过期,然后呢,此时正好十几万个都查他们的请求同时过来,我们就不用考虑这个。
02:19
问题,而且我们说的之前的说加随机时间,用这个解决加随机时间其实这个东西呢,很容易弄巧成拙,比如呢,我们第一个数据是三秒以后过期,我们给他加了一个随机,比如我们加了一秒,是四秒后过期,第二个数据呢,是两秒过期,我们又加了个随机,加了个两秒,正好也是四秒过期,本来我们啥都不加的时候,人家两个过期时间没有冲撞在一起,结果我们有可能一加倒还让他们密集性的冲撞到了一起,所以呢,我们说的这个,只要我们加了过期时间就行,我们当时每一个数据存储的这个时间节点它是不一样的,我们这个时间轴呢是分散的,然后大家加了这个过期时间,所以他们过期的整个时间也是分散的,所以我们这个我们来加上过期时间,只要指定过期时间就行,包括有的同学说我可以永久不指定过期时间,我让数据永久存储,当然这也没问题,当做好数据一致性保。
03:19
证就行了,所以我们现在要做的办法是加上了过期时间,给每一个呢都指定了,所以我们spring boot里边也有配过期时间相应的配置,然后特别是我们的这个写模式,这个写模式的情况下,我们说为了保证主要缓存与数据库的一致,与数据库一致,我们说了好几个方案,第一个我们可以读写加速,我们让它有序进行,这是我们说的第一种,但这适用于我们读多写少的系统,如果写读多了一直在那加锁等待,那也不合适。还有我们说的第二种,那我们可以直接引入中间件canon,我们引入canon,它来感知到咱们这个my circle的更新,然后去更新数据库,那这个呢也是可以的,或者我们说你这个读多写多的,读多写多的你就直接。
04:19
直接去数据库查询就行,所以对于写模式对一致性的要求,我们来看我们spring put整合的这个catch有没有相应的解决方案。好,那么现在已经探索到了spring catch的这两个解决方案,就是缓存公数据以及我们可以指定过期时间,那么就来看缓存击穿问题,它有没有解决。那缓存击穿问题呢,就是我们大量并发同时来查这个数据,这个数据正好在缓存中没有,我们看它会不会将所有的请求都放去来查数据库,那么就来看这个catch注解,Spring catch是怎么解释的,那说这个呢,我们来简单再来捋一下原理,捋一下原理我们就知道该分析哪些方法。原理呢,就是spring catch里边有一个东西叫catch manager缓存管理器,它可以帮我们造出很多的缓存组件,而这个缓存组件它是负责进行缓存的读写,他负责我们缓存的读写。
05:19
那正好呢,我们现在整合red,我们用的这个catch就是red的catch manager red的缓存管理器,它能帮我们造出的这些组件,比如这就有一个叫create red sc,它给我们创建出的是red catch。所以我们在这一块我们用的是red sc,包括manager呢,我们用的是也是red catch manager,那么接下来就看red sc manager里边给我们创建的red sc,它是怎么操作缓存的,那么直接找到red catch的源码red catch,我来选中这2110,好,随便点进来,那么这个red sc里边呢,这是它的这个源码,它里边定义了对缓存的很多操作,这是它的构造器。好,我们来看下边lookup,这是一个查询,我们可以给这些方案都打上断点,然后咱们继续往下看,我们发现,诶这个get,我们发现这个get方法呢,它加了一个synchron,相当于加了一个同步锁,这个锁虽然加的不大,不是分布式锁,不像我们以前分析们要锁住所有服务,我们来加一个分布式锁,来看一下我们想要锁住所有服务,我们在这来加一个分布式锁,但是它没加分布式锁,它加的相当于Z锁,但其实就算加了this锁也会好很多,我们服务呢,假设有十个。
06:35
每个人只锁住他当前自己的都没啥问题,那最多呢也就放我们这八九十个请求,我们去数据库也没啥事儿,所以呢,我们这一块确定有一个加了锁的get方法,我们就看我们缓存查询的时候有没有调到,包括其他的缓存更新,我们都提前打上这些断点,还有这些更新的这些断点我们来看什么时候用,看是evi的缓存删除,缓存清空及下边我们来看还有没有什么缓存增删改查的操作,好这一块呢,都没有,都成了序列化,反序列化了,行,我们这一块呢就不看了,那么现在以debug模式来看一下,我们来查询这个数据的时候会不会走那个加锁方法,好,我们来return in debug mode,我们先把缓存里边的数据我们给它家大家清空一下,然后我们来主要来看们断联的时候呢,会不会过我们这个枷锁的这个方法,好,我们把这一块控制台我们来清空。
07:32
来准备来要数据,我们在浏览器上,我们就要当前项目的这个首页,我们就发一个请求就行了,好把这一块清,我们来监控一下,我们现在来要首页数据回车,那首页数据过来以后呢,他先去查,要查的这个件是这个先从缓存中查。诶,我们发现呢,它调用的是lookup方法,没有调用我们这个加锁的get,好,我们先查吧,这个lookup方法我们来往下放行,我们发现第一次从缓存中查询的是空的啊,没有东西,那就直接返回空,那你返回空以后,我们这个lookup就结束了,那么继续往下走,我们来看它掉了什么,它掉了相当于缓存这个执行器。
08:15
他之前调这个catch的时候,Get方法的时候,相当于得到的结果为空,我们来走,那相当于他上一步就是调这个方法,刚才进去的调用do get从缓存中拿取东西,拿到的呢,现在为空,如果不为空的话,直接返回为空的话,返回为空啊,我们现在返回为空,只要缓存中得到的这个数据,我们来看他呢,查询缓存还是调这个方法放的incas,在缓存里边先找数据,因为我们这个方法啊,之前都已经运行完了,现在都是回跳过来,那这块是空的,缓存当中的数据是空的,如果不是空,直接返回是空,还是一连串往回返回空好,那返回完空以后,我们继续让它返回空,整个相当于我们这一块叫catch hit,缓存命中,那命中呢,那就没命中,这就是一个空,那只要没命中,那就开始来调用各种方法,如果缓存是空,它还收集一些缓存更新的操作等等,这些我们就不看了,这些缓存不为空怎么着,我们现在缓存都为空,那为空呢?接下来它就会有一个叫return。
09:15
六他在这invoke相当于执行我们这个方法,那这一块我们如果step into进去的话,就会执行到我们这个目标方法就会调用这个了。那看我们现在方法在这儿,它是调我们这个相当于批注,是执行我们这个方法,得到我们真正的数据,如果缓存没有命中的话,那如果我来放行这个方法,那就应该来到我们真正的业务逻辑,好,我们把这一块呢,其他线清空掉,来看一下来放行这个方法来我们确实来到业务逻辑了,那是我们放行上一个方法过来的,上一个方法我们来看一下,就是我们刚才的那个缓存命中的这个方法,诶。现在。我们已经放行不见了,行,先来到这个方法吧,那么这个方法执行结束以后,来让它结束真正查到数据以后。
10:05
他最终呢,给我们进行返回。我们直接来返回我们这个方法呢,相当于就执行完了,好我们把这一块呢,执行完给它先一直返回我们这个return value,先执行了目标方法就执行完了,执行完了以后,我们目标方法就有返回值了,那有返回值以后,把我们这个返回值再来进行包装,包装成我们缓存里边要放到这个值走,那接下来呢,它就会给缓存里边来放这些值,那我要直接放行这个方法就会运行到我们之前给red catch来打到断电这一块,好。它给缓存中放数据,肯定会调用我们缓存的put方法,好,那现在呢,先来到这一块,这一块整个方法呢,都是在缓存切面的,相当于这个支持器里边,缓存所有功能都是拿a up做的,那它这有一个缓存切面,好我们直接呢给他放行,如果放行呢,他现在应该是将缓存的数据我们放到缓存里边,这是我们方法返回的数据,它就应该给我们放进去,好我直接来放行,我们发现确实掉了red catch的put方法。
11:10
跟我们之前编写的业务逻辑代码都是一样的,当然从整个流程里边,我们并没有发现任何加了锁的操作,好我直接全部放行,那么这个操作呢就结束了,所以你会发现我们这个spring catch里边并没有给我们默认的进行加锁,所以们缓存击穿它没法解决,默认是无加锁的默认是。那如果我们想要让它解决缓存击穿问题。两种办法,第一种我们不用spring catch了,自己手写那一堆我之前的缓存代码。第二种想要用它了,来看这个catch Apple注解里边,它有一个属性叫SYNC,诶,默认呢是false,也就是说我们这个方法是不是同步方法,默认呢是false,如果我们把它改为true。
12:01
我们来看一下这一块的备注,这一块呢,就会说它呢,同步相当于同步我们这个哪些线程啊,一系列的线程去尝试加载数据,加载我们相当于同样的数据,他们是拥有相同K相当于,那就是对单个相同的这个数据,那全部放进来都是查一个数据的,那我们把这个数据呢,在这儿利用它给它进行加锁,它即使加成本地锁也足够了,本地就算有100个服务,顶多也放100个过来都可以,不用加分布式锁好所以我们加了这个以后,那现在来重新以第八个模式启动,以前不加,我们发现整个调用从缓存中查询方法是无锁状态的,那现在加了这个以后,来看会不会调用我们这个red sc,它里边提供的这个加锁的get方法。那如果是调用是怎么调用的?来看一下,当然这个方法要调之前,我们先把缓存清空来模拟整个的流程。
13:01
好,我们现在来看一下这一块的效果来进行测试,那现在还是来请求首页数据,我来刷新,那这个首页数据呢,默认没有,诶我们发现现在终于进来我们这个加锁的方法了,那加锁方法呢,它先调用第一个get,第一个get就是我们之前进入到这个里边,我们用lookup来查值的时候,所以呢,它调用第一个get,那相当于它不会查到我们这个数据来过来。那我们缓存中没有它第一个get呢,就是调lookup方法的,好,我们来给它返回为空,走,我们直接返回为空,先来调第一个get,那么缓存中没有它这一块判断如果缓存不等于空,现在缓存中有,那就直接返回,缓存中没有它在这呢,有一个叫value from load,从我们这个加载器里边读值,按照这个K读值,将读到的值读到以后再放入缓存里边,诶这跟我们缓存的读模式的代码一模一样,缓存中有了返回缓存中的,缓存中没有了,去来读取值,来看这个读取值是怎么读取的,Step into这一块呢,相当于利用一个cornerable传入一个相当于我们有返回结果的这个异步线程的方式来去来调的,它最终给我们读到值,那么正好看一下这个扣方法,它是咋运行的?来,Step into进来。
14:23
那么这个call方法呢,那就是这一块的代码,我们再来step into进来,他在这in work operation,其实这个我们之前见过,包括整个这个方法我们都见过,这个就是放行来执行目标方法的,我们可以给大家放行一下看,走诶我们一放行来到执行目标方法,那整个执行完了,我们相当于那一块的代码呢,就结束了,我们让它执行好,执行完了我们来继续,我们让方法返回过来,好,返回继续返回,继续返回,多点两下,好,我们刚才这个方法呢,就返回了,相当于如果我们加了那个属性,它判断如果是同步代码的话,那就会调用我们这个缓存的这个被锁定的这个同步方法,来看一下我们的这个red catch red catch。
15:09
我们这一块呢,就会掉catch.get2个参数的方法,那catch.get2个参数的我们来看一下,就在上边我们加了锁的方法,我们给它打断点了不加锁的,它是只传了一个K,加了锁的呢,它是传了两个参数,一直看下边好,一个是K,一个是我们这个poable,相当于我们这一块呢,缓存中没有查到,它从我们目标方法里边读到值,好。刚才这个方案就运行结束了,我要一放行,那就来到这儿了,这是我们目标方法给我们的返回,然后呢,他将目标方法返回的数据调用它本身的put方法,相当于给缓存中再保存一份,给缓存中保存就是拿这个writer缓存中写的这个工具,先去将ke转换,再将Y6转换。然后给里边存数据就行了,但是我们发现缓存的整个保存方法也是没有任何所得,我们对缓存里边所有的方法浏览,除了这个get方法有锁外,剩下全部无锁,而且有锁的get方法就是我们以前编写的模式,缓存中有返回,返回中没有调用我们目标方法,查到以后放到缓存再返回,好,我们现在来放行,那整个方法呢,至此就结束了,那我们来总结一下我们的spring catch,来到这个实现里边,Spring catch呢,我们加了这个SYNC这个属性,而且这个属性呢,只有catch注解里边有,其他注解里边,比如我们来看一下catch evict,那么进来YG里边呢,是没有我们这个同步这个设置的,所以说呢,只有我们这个查询的时候它有设计,我们这个是否要加锁,我们编写一个它那就进行了加锁,我们就可以来解决击串问题。
16:56
加锁解决气穿。我们也无需加那么大的锁,加完整的分布式锁,我们加一个本地锁就已经足够使用了。都读模式spring都考虑到了,但写模式其实是根据自己的业务代码不同情况要去不同执行的,所以呢,写模式spring catch没有管,所以呢,我们来总结一句话,总结。
17:23
要说我们这个常规数据的缓存使用,我们完全可以使用catch常规数据,那也不考虑缓存数据一致性什么问题的,我们读多写少的常规数据,我们读多写少的一致性实时性要求不高的,读多写少及时性和我们这个一致性要求不高的数据,我们完全可以使用SW catch,就算他写模式没有考虑到加锁,反正数据时间一到以后就清空了,我们下一次主动又能查出最新的数据,所以我们完全可以完全可以使用spring。
18:04
但是如果我们这个实时性要求高的么,这些要求高的数据,那么这个特殊数据,特殊数据我们还想加缓存,特殊数据那么还想加缓存提升它的速度,还想保证一致性,那么肯定特殊数据就得特殊设计,那么以后业务用到这些特殊数据了,比如要引入canon了,我们要加读写锁了,或者我们要加排队的一些公屏锁了,我们都可以来做这些事情,所以呢,我们常规的一些数据,这菜单数据之类的,我们以前分析的所有东西,我们都可以不考虑用了。spring读模式,缓存的三个问题只要解决了就行,写模式在写模式常规数据情况下,常规数据写模式只要有,只要缓存的数据有过期时间,过期时间就足够了。哪怕呢,过期了,触发一个主动更新那就行了,所以呢,这是我们对常规数据的处理,我们直接用spring catch,特殊数据,我们业务以后用到,我们再来特殊设计,那么整个spring catch们就讲完了,我们以后在业务系统里边,我就直接用那些没用的注解,我们catch put没用,包括catch conf这个也没用,我们后面用到的时候,我就直接用就行了,Spring catch也能非常极大的简化我们的开发。
我来说两句