00:00
好,接下来我们就来看如何正确的枷锁解决我们缓存气穿的问题。首先我们先来考虑加锁,加锁的位置就是在我们这一块,当我们第一次来查询缓存的时候,如果缓存中没有假设这有100万的并发,同时判断没有,那大家都要去查数据库,那这一块呢,我们就不能让他都来做,所以我们把这个枷锁呢,我可以写在这个逻辑里边,Get catallo杰,Son,我们在这儿,那枷锁呢,有这么两种方式,我们可以先暂时考虑一下,第一个来写同步代码块,Synchronize的,然后呢,我们把这一段的东西,我们全部放在我们这个同步代码块里边,好们把这一块返回,我们都放在这里边,然后呢,接下来加锁,我们就要考虑这一块加什么样的锁,加锁呢,我们唯一需要考虑的一个问题就是,我们只要是同一把锁,只要是同一把锁,那就能锁住,就。
01:01
能锁住咱们需要这个锁的所有线程,锁住需要这个锁的锁有线程,所以呢,我先来考虑第一个我在这呢,如果写了一个this this呢就是代表当前对象,所以呢,我们相当于使用当前对象加锁,那这个能不能锁住呢?我们来考虑一下那第一种,那这个synchronized的使用this的方式,那我们这个this呢是当前对象,那正好我们来想象一下我们整个spring BOO的流程,首先呢,我们这个spring boot spring boot,我们所有的组件,所有的组件,所有的组件在容器中都是单立的,所以呢,我们接下来看到的现象就是我们即使有100万并发进来,我们调我们category service这个接口的这个方法,因为我们这个service呢,只有一个实例对象,所以。
02:01
这this呢是单立的,所以我们这个this呢,相当于我们这100万个请求,同时都是用同一个this,那我们就能锁住了,哎,这是我们说的单立的。那我们这么来写是可以的,我们呢也可以来这么来写,直接给方法,这来加一个synchronized,这两种呢都行,好,比如呢,我就来加到我们这个代码块里边,但接下来正确的逻辑就应该是好,我们这100万个呢,请求同时进来,比如来到我们这一块,好同时进来,那将要调用这个方法,好现在100万个呢同时调进来,但是一进来呢,我们上来就先锁住,接下来呢,这100万个就得给我们竞争锁,假设有一个竞争上来了,那他呢就去执行数据库查询,查询完了以后返回整个方法结束释放锁,那别的请求再一进来,然后他如果再去来查数据库,这就是不合理的,相当于我们虽然锁住了,但是他们都是排队再去查数据库,所以呢,我们锁住了,你拿到锁以后进来要做的第一件事儿就是再看一下缓存里边有没有,如果有了,说明可能是上一个人执行完。
03:14
放好的,如果没有。那你再才需要去查,所以我们把这一块呢,得写一个正确的逻辑,就是呢,我们加锁以后得到锁,得到锁以后,我们应该再去缓存中能确定确定一次。如果没有才需要继续查询,所以呢,我们在这来做所有事之前,我们再得一遍缓存来获取缓存的数据,那就是这一块,把这一块呢拿过来走,我们还是判断if,如果我们这个category Jason,我们写一个string UUS,点一个it's empty,我们从缓存中得到的这个数据,它是空的,哎,我们才有必要往下进行,如果不是空的,我们就直接给它返回这个结果就行了。好,我们把这个结果呢,也在这给我们转换为对象,我们直接返回。
04:16
相当于呢,这一次就有数据了,如果缓存不为空,直接返回。否则我们才需要在数据库来进行一次,这是我们说的加锁的完整逻辑,但是这样加锁合适吗?如果在单期应用的情况下,也就是我们这个项目只会部署在一个tomcat里边一台服务器,那这样加锁没问题,但是如果分布式那就变了,来可以看一下分布式的场景,如果分布式呢,我们常见的就是会把商品服务好,我们全部放在好多台服务器,假设呢,我们大并发过来,由于负载均衡机制,现在呢,每一个商品服务它呢都接受了1万的并发进来,而我们加锁呢,加的是this,我们来看一下,那现在呢加了一把锁,这个锁呢,它指的是this,好,来写上这个this,那this的意思是什么?那就是当前实例对象,那我们首先就得考虑一下这个this,无论我们是给同步代码块上加的this,还是方法上的this。这个都是当。
05:24
当前实例作为锁的当前实例呢,在我们容器中是单实力的,但是呢,我们一个项目一个容器,也就说明呢,我们一个商品服务一个容器,那这样呢,我们商品服务有十台机器,八台机器,那相当于我们有八个容器,那八个容器呢就有八个实例,所以呢,每一个this只代表我们当前实力的这个对象,也就是呢,每一个this都是不同的锁,那相当于呢,我们来加,那最终加了八把锁,那最终导致的现象就是我们商品服务一个Z锁,相当于把1万请求锁住了,只有一个放进来了,然后呢,第二个商品服务我们二号机器的也相当于只锁住了他1万个,把一个放进来了,相当于呢,我们最终还在分布式情况下,我们有几台机器,那我们至少就放了这么几台机器的线程进来。
06:18
那么现在相当于还是有八个线程同时进来去查数据库相同的数据,所以我们说本地锁只能锁住当前进程,而我们现在要真正实现我们这大并发80万进来,我全部给锁住,只留下一个进来查询数据库。我们就需要用分布式锁。当然分布式锁带来的缺点就是它的性能比较慢一点,而我们当前的这个进程锁,我们本地锁它呢稍微比较快一点,但是呢,我们本地锁的缺点就是在分布式情况下锁不住我们所有的服务,但如果基于我们这种场景,我们用这个同步锁可不可以呢?也是没问题的,我们使用这个本地同步锁,顶多呢,我们商品服务哪怕放上100台。
07:08
现在呢,有1000万的并发,我顶多给你放100个进来查数据库,那这样呢,我们数据库压力也不是很大,而且我们所也不用设计的那么重量级的,所以呢,我们可以先来演示在我们本地锁的情况下,我们这一块代码能不能运行,那我们就来写上我们本地锁的逻辑,那本地锁呢,我们先留在这,我们说一个问题,最大的问题,那就是本地锁,就是我们以前无论是我们知道的synchronized这种锁,还是大家用guc包下的return lock等各种锁。那我们这种锁呢,都是我们称为本地锁之锁,我们当前进程的,也就是说我们这个product商品服务,包括我们去解微收web里边,我们来看一个服务呢,对应一个完整的进程,我们连接上这个进程,能观察它的线程以及它的内存等等,所以呢,我们以前的这些锁都是我们这个进程锁,只能锁住我们当前进程里边的这些资源。
08:10
也就是说在分布式情况下,我们说在分布式情况下。想要锁住所有,想要锁住所有,我们必须使用分布式锁,而且我们刚才也给大家分析了这个场景,那好,那正好呢,我们就用这个场景给大家演示一下,行,我先用本地锁,本地锁呢,只要在这一进来,我们就在控制台打印一遍,我们查询了数据库,好,This out,我们就叫查询了数据库,我们就来看一下我们并发的情况下,如果我们来加了锁以后,还会不会所有人同时来查数据库。我呢,把这两个重新启动一下。把这块代码呢,简单写好以后,我来重新启动一下们加了一个锁,凡是想要真正执行我们业务逻辑,查数据库之前,我们呢先给它加上锁,得到锁了以后,如果缓存中没有再去查数据库,如果有了我们就不用查了。
09:13
我们把这段逻辑加上以后呢,我们来测试一下。我们先来访问,保证我们这个接口呢有数据,而且呢,为了模拟出他查数据库,那我们一定要把缓存中之前的数据要先删掉,好我们先来看这一块服务呢,现在启动成功,启动成功们先来运行一下刷新,那现在呢,这是从缓存中得到的,我们在这儿来判断一下。这块呢是有一个if,如果进到这,我们就来输出,叫缓存不命中,缓存不命中,然后呢查询数据库,然后如果能进来,那就是查询数据库,否则的话这个if进不来,那下边呢就是缓存命中,那就不用查询数据库了,缓存命中好直接返回,我们就叫直接返回,那么现在来压力测试,我们这个效果好,我们来重启一下我们这个商品服务,来看控制台打印,那我们来进入咱们这个压测流程,还是呢,我拿50个线程来在这儿来进行压测,那大并发呢,当然我们来可以加到一百两百也都行。
10:21
比如我现在来就来加上100,我们现在先来访问一下我们这个首页,刷新一下,好,现在呢有数据,有数据呢我们一定把我们这个先删掉,Reload好来把这个呢先删掉,我把这个删掉好点一个yes,那我们之前查询呢,是有数据的情况下,所有缓存命中直接返回,现在呢,我们缓存里边已经没数据了,来刷新一下啊,确定没数据了,然后呢,我们来给它进行压力测试,那我们想要做的效果就是我们在控制台一旦打印缓存不命中,我们在这儿呢查数据库,数据库呢只能查一次,如果查了多次,那就是这个加锁是失败的,那么就来进行一个压力测试,好,那现在来直接启动。
11:04
好,现在呢,我们有100个同时来压力测试,那现在吞吐量是170多,那加了缓存以后呢,吞吐量肯定会更大,我把这个停掉,我们来看一下我们这个控制台查询了几次缓存,好我们来找一下,这块呢,都是缓存命中,直接返回来,一直再来往上,我们再来往上,我们来一直来找,看我们有没有,我们哪一次是来查数据库的,好我直接在这来搜,有没有调用查询数据库的方法。CTRLF,我来找这呢,有很多缓存不命中查询数据库,那这就是我们一进来大家都看缓存不命中查数据库,当然我加了锁,一加锁以后呢,来到这查询数据库呢,我们是这个打印,我们来找一下CTRLF,好,我们发现呢,这有一次查询数据库,我们再来找一下看还有没有,诶我们发现呢,这还有一次查询数据库,那么发现呢,现在有两次我们这个查询数据库,并不是我们说的数据库,只查了一次,为什么会导致这个问题?
12:05
我们来分析一下,为什么他会来查询两次数据库,首先看我们这个业务逻辑,我们再来做所有事之前,我们先去查一下缓存,然后呢,判断缓存中没有,也就没命中的时候,我们就去调方法来查数据库,然后这个方法呢,上来就会进行加锁,所以我们整个呢业务就是这样子的,我们先在这儿来查缓存,如果缓存中没有,我们就进来查数据库来进行执行,那整个方法呢,我们是加了一把锁,我们黑颜色呢就表示这是个锁,然后呢,我们一定要进来查数据库,之前我们都是在里边先判断一下,我们再次确认一下缓存里边呢,确实没有,如果确实没有呢,我们就给他查询数据库,所以我们来到这个方法,缓存确实没有,我就去查数据库,数据库整个在下边查询完,组装完,我们返回结果,返回结果呢,我们这个方法,整个锁结束,我们就释放了我们这个锁,所以呢,我们这个方法。
13:06
整个数据库查完,我们释放锁,最终释放了以后,我们这个方法调用成功,将查询到的结果放到我们这个缓存中,所以我们接下来将这个结果呢又放到缓存中,就这么一个业务顺序,导致我们查了两次数据库,相当于我们没有锁住,那为什么会两次数据库来分析一下,首先呢,假设我们这100万并发全部进来,我们看哪一块能进去数据,好我们在这呢,我们来分析,这有100万,假设呢,这有100万并发,100万并发呢,我们进来先去来看我们的缓存,大家呢,都进来看缓存,缓存里边呢,都没有假设呢,都走到这一步,好,缓存中没有都打印没命中,准备去查数据库,然后呢,查这个数据库的时候,我们上来就给他锁了一把锁,所以说呢,能进入到我们这个方法里边。也是说确认缓存查数据库,那肯定呢只有一个线程,好,我们进入到这个方法里边来插入,现在呢,确实只有一个线程进来了,因为我们锁住了,我们在这儿呢,判断缓存中确实没有,我们也给他查了数据库啊进来了,那查完了以后呢,我们这个线程在这儿干嘛。
14:20
只要他做完事儿,他就要释放锁好,我们这是第一号线程,我们来放在这儿。第一个请求我们就叫一号线程,然后呢,他查完以后呢,在这儿结束了以后释放锁,也就是说他在这一块就已经把所释放了,那接下来我们这个所住的其他号线程,那二号线程就再进来一个,好,我们再给这儿呢放一个二号。那只要他在下一步做完事以后呢,他就放二号,然后呢放二号,二号进来,他先要确认缓存有没有一号,一释放锁以后呢,他要给red里边放我们数据,但给red放数据,这是一次网络交互,可能呢很慢,包括我们刚启动起来,我们要给red建立连接,还要整线程池,这一堆操作线程池等等都还没有初始化,所以这是一第一次来做,是一个很慢的过程,那就会导致什么现象,我们这个人在这儿把锁一释放,第二个人进来确认缓存中到底有没有,他看缓存结果,而上一个人释放锁了以后,给缓存中放,假设呢,还要有30毫秒的时间,那就是这30毫秒他在这儿确定确实没有,也就是说他还没来得及放进去,他就在这儿呢,从red中获取,确实没有,那就导致呢,他又打印了一遍查数据库。
15:44
他打印完以后呢,又释放锁,三号呢又进来,可能呢,三号刚进来,一号的数据才放完,二号的数据还没放完呢,所以三号判断缓存中有没有可能判断的还是一号这个线程之前给里边放的这个数据,所以这就会导致我们查了两遍数据库,那只要他查一遍数据库,我们只需要做一件事就行,把结果放入缓存,我们不要在释放锁了以后做,我们只要查到了数据库,我们呢就把结果放到缓存,所以我们就应该放到上边,这才是一个真正完整的合理的逻辑,不会导致我们锁不住查两边数据库。
16:29
那最终呢,来到这,那就是方法结束,所以呢,我们这个方法的完整改造,那就应该把我们这一块调用我们这个方法去数据库查,查到以后呢,直接返回。而且查到以后我们把结果放缓存。也一定要把它跟这个方法放在一起,好,我们就在这儿,这是最终返回结果,那在它返回之前把这个结果序列化,再放到缓存里边。
17:00
这样呢才是一个合理的逻辑,包括查数据库,到底是哪个线程查了数据库,大家也在这儿可以打印一下线程号,确认一下就行,好,我们把这个逻辑改了以后,我们重新启动,看一下我们这个缓存不命中,将要查数据库。我们之前呢,查了两遍数据库,现在呢,我来重新启动,我们看现在是否查一遍。我们先把缓存里边的数据我们来删掉,我们之前压力测试缓存里边放数据了,而且有一天的过期时间,好,我们把这个数据呢,我们先来删掉,我们再来给他进行压力测试,我们来选择delete yes,好,我们来reload看一下,好,现在没有数据来进行压力测试,测试之前我们把控制台整个清空来看我们这个查询数据库会打印几遍,好。我们还是拿这100个并发,我们来同时来进行压力测试,这是100,好,我们来进行运行。好,来压力测试开始,我们来看控制台,这缓存命中直接返回了,好,我们来给它停掉,走,我们看汇总报告,吞吐量呢也挺大的,我们来看控制台有没有查询了数据库,这个CTRLF来搜查询数据库,这有一个我们再回车,诶发现呢,现在就只有一遍查询数据库。
18:19
那这就是我们说的,我们一定要保证我们的这两个操作,也就是查数据库,查完以后放缓存,这是一个原子操作,比如说在同一把锁内进行的。否则就会导致我们释放所的整个时序问题。导致我们查了两遍数据库,好,那我们呢,就是使用本地锁,先改造了一下我们这个方法。我们发现呢,这是可以运行的,那下节课我们再来看一下我们这个本地所,基于我们以上分析,在分布式情况下会有什么问题。
我来说两句