00:00
好,前面呢我们也给大家演示了一下reddison里边的其他分布式锁,那以后我们分布式系统里边想要加分布式锁,我们都来使用reddison就行了,而且reddison最大的好处就是它的每一个方法,我们来随便点进来,来看一下它在这的调用,比如counterdown anthic,这都是一个落瓦脚本,所red呢底层的所有锁都保证了原子性,而且reddi呢也基于它的开门狗机制,也解决了我们死锁问题,那回归我们的业务代码,以后我们业务代码里边想要加分布式锁,我们就可以不用写这一块的内容,我们直接使用RA在这来进行lock lock住了我们来执行方法,我们也不用写没lock住的逻辑,因为我们在这lock是一个阻塞等待,等待完以后我们来执行就行了,那我把这一块的代码呢,我就来再次变化一下,好,我们把这一块代码我来复制过来,如果我们现在来使用reddi来做我们这个事情,我们就不叫red lock了,我们就。
01:00
找reddi lock,那使用reddison来做呢,那就是这样么?占分布式锁,我把这个删掉,CRLYCRY,然后呢,包括加锁失败我也删掉,我们无需这个加锁失败,包括加锁成不成功这一块我也删掉,好那我们现在的整个做法就应该是首先来获取分布式锁,我们直接拿到我们的reddi,我们先得注入我们的reddi,好,我们在上边,我们将我们的reddi client,我们来注入过来,我们就叫reddi。Owa,我们以后的所有分布式锁,我们就直接拿它来加,来到我们这个方法,Red look,好,我们先要战锁,那就第一步,Red直接点一个,我们就叫get lock。我们想要占一个我们这个分布式锁,假如这个锁的名字就叫看log杰森lock,好,我们要锁这个数据的锁,哪个数据我们来就加上这个数据,再来加上这个lock,而且这个lock呢,大家一定注意我们这一块lock传的这个名字,只要名字一样,那我们的锁就一样,所以大家一定要注意我们这个锁的名字。有的同学说这个锁的名字有什么注意的,那举一个例子,如果大家写习惯了以后,获取分布式锁都叫get lock,我们传个名字就叫lock,那现在就出现问题了。
02:22
我们获取分类数据,如果获取分布式锁也叫lock,获取品牌也叫lock,那们获取商品也叫lock,那相当于我们这一把锁将三种不同的业务都锁了,所以呢,我们这个锁的名字就牵扯到了我们一个叫锁的力度,我们这个锁的力度,锁的力度越细,这个力度呢,越细,我们锁的资源越少,肯定运行起来就越快,我们这个越快的力度越大,以后全部都用lock,那业务里边全都锁住了,那就越慢,所以呢,我们一定要注意业务期间锁得力度的选择,那么一般就是这么来约定的,我们来约定一下所的力度,如果我们具体缓存的是某个数据,具体缓存的是某个数据,比如我们就在缓存里边保存的是11号咱们这个商品,那么锁的力度呢,我们就应该是这样,我们假设11号商品现在要加。
03:22
锁防止缓存击穿问题了,那么就应该叫这个,我们叫product代表呢,这是商品杠,11代表这是11号,然后杠lock,那这是11号商品的锁,那么与此同时还有12号商品的锁,诶不是说我们所有商品都只加一个商品锁,那这样我们力度一加息以后,比如11号商品缓存中没有了,那现在100万的并发全部进来,那加的是11号商品的锁,相当于对11号商品我们只放过了一个,要不然是以前的这种,比如我们加锁加成这种,大家来看我来加成这种,跟我们来指定好加哪个商品的锁有什么区别呢?假设我们来加成这种,我们11号商品缓存里边没有了,12号商品缓存里边也没有了,设呢11号现在是一个小并发,现在只有十个请求,12号呢是一个大并发,有100万个请求,那如果我们来加这个product lock锁,我们现在来请求11号商。
04:22
同时别人也在请求12号商品,我们会发现这个12号商品跟11号商品其实没啥关系的,但是他俩用了同一把锁,那么就得等待12号商品这个分布式锁释放了,我们才能查11号,那这就是一个很奇怪的操作,所以我们的锁的力度一定要设计精细,那最终我们的这个锁呢,由于这是一个分类的解S数据锁,那我们就叫catallo解s lock,那从reddi里边拿到了这把锁,拿到这个锁以后呢,我们直接lock,点一个lock,现在我们来给它进行加锁,而且一加锁以后。下边所有的代码都是一个阻塞等待,那么就把这一块的所有内容我全部删掉,我们就应该把下边的所有代码我们全放到串里边,好,我把这一块呢,我也删掉。
05:09
这是我们最终返回数据,包括结束了我们删锁也不用这么来删,好ctrl all,我来代码整理一下,我们有了red,我们先在这来加锁,一加锁以后下边的过程就被锁住了,想要执行呢,就必须等待我们上边的锁释放,执行完成以后,我们在这来使用final,在这来解一个锁就行了,就来写一个lo,点一个unlock。这就是我们使用red,我们就将代码呢简化了非常多,但是呢,我们现在又出现一个问题,我们现在呢,读所有的数据,我们都是来先来看缓存,缓存中没有呢,我们来去来读数据库,我们会调用我们的这个方法,那这是我们第一次,我们数据呢,刚添加进来,我们没从来没查过,那缓存中就没有,那现在我们再来考虑一下,如果我们这个数据修改了。比如我们这个三级分类的数据,我们这个是返回所有三级分类,我们这个方法呢,正好在前边呢,有一些三级分类的数据修改,诶比如我们在这更新了一个三级分类数据,那更新以后呢,我们三级分类的信息肯定就变了,我再从缓存中拿到的这些就是一个旧数据,所以接下来又牵扯另外一个问题,就是我们说的缓存里边的数据,缓存里边的数据如何如何和咱们这个数据库保持一致,这就是我们说的缓存数据一致性问题,这一致性问题呢,我们现在用的非常多的两个场景,一个呢叫双写模式,还有一个叫失效模式,这两个是现在用的非常多的模式,那他们是怎么工作的?来给大家看一个原理图,首先,如果我们使用双写模式,我们对数据做了修改,比如我们修改了一个菜单。
07:00
然后red里边的数据想要变,我们可以这样,我们再来改完菜单以后,我们把缓存里边的数据我也改一下,就像我们在这,我们即使呢,改掉了一个菜单,诶我们在这在上边我们将这个菜单呢改掉了,我们也不知道改的是哪个菜单,那改完了以后呢,由于我们缓存中缓存了所有菜单,我们应该同时修改咱们这个缓存中的数据。但这样要修改呢?相当于我们把所有菜单还要查一遍,再给缓存中放,太麻烦了。这是我们说的咱们这个双写模式,就是我们把数据库改完,我们就跟着把缓存一改,还有我们说的一种将失效模式,我们把数据库改完,我们直接将缓存删掉,那这样做的好处是什么?比如我们在这儿将数据库改完了。但是我们缓存中还缓存了我们三级分类的所有数据,们可以在这儿直接删掉我们缓存中的数据,比如delete,我们当时呢,缓存了整个三级分类数据,我们名字,比如我们就叫catallo杰son,那我们就把这个数据呢,我们给他一删,一删以后呢,缓存中相当于就没有数据了,那下一次再来查这个菜单,我们查询方法呢,就会判断缓存中没有数据,我主动查一次数据库,相当于呢,我们删的话就会等待下一次主动查询来更新就行了,等待下次主动查询进行更新。
08:24
但这两个模式呢,在大兵发下都会产生一些漏洞,比如我们来给大家分析一下,我们先来说双写模式,我们的想法是写完数据库,我把缓存里边的数据跟着改一下,那假设我们现在就模拟两个并发,同时再改一条数据,我们比如改ID唯一的菜单,我们把手机呢,我们就叫新品手机,那现在我们来看两个并发全部进来,但是呢,我们第一个先来执行,他先去改数据库,他把这个数据呢改成了一,比如把手机改成了一,他把数据库改完了,然后此时呢,第二个修改请求,还是改这个菜单进来,他想把数据呢改成二。
09:05
好,它改成了二,相当于我们最后一次对数据库的修改模式二,但如果我们使用双写模式,我们第一个人呢,改完数据库我们要改缓存,但是可能由于各种原因,他改完数据库以后呢,CPU没转到他这儿。或者他卡顿了一下,或者他的机器比较慢,比如第一个请求落到一号机器了,第二个请求落到二号机器了,二号机器呢离red特别近,一号机器离red比较远,他想要写缓存,结果没有人家二号机器跑的快,二号机器呢,改完数据库快,直接把缓存直接改了,改成了二,但此时呢,一号机器才慢慢腾腾的将数据呢写到缓存改为了一,那此时我们缓存中就保存了一个一号菜单,它的数据是一,但实际上我们一号菜单呢,最后一次更新是二,而不是一们相当于缓存中就出现了一个脏数据,所以呢,我们说双写模式会有产生脏数据的分享,那怎么解决这个问题呢?我们来可以考虑一个设计方案。
10:08
就是加锁,比如我们产生并发写的时候,因为要写数据库,同时要改缓存,那么就对整个操作我们来加一个锁,如果一号先进来了,那一号得到锁,只有一号把它的整个流程全执行完了,二号才能得到锁去来写它的整个流程,这就不会产生我们说的数据不一致问题,这是第一种方案,那第二种方案看我们业务允不允许,我们会有暂时性的数据不一致问题。举一个例子,我们商品的这个菜单数据改了,比如京东在后台改了,可能五分钟、20分钟或者一天以后。我们的整个网站里边,首页展示才是我们改了的数据。我们允不允许这种操作呢?如果允许我们就可以不管这个事情,那不管怎么办,就是我们以前。
11:01
将数据放入缓存的时候,给每一个数据都给上一个过期时间,比如我们是一天只要这个数据呢过期了。我们从red里边就会自动删除,那删了以后呢,下一次再来查这个数据请求,就会查一份新的再放进red里边,所以呢,我们也可以称为这是暂时性的脏数据问题。当然前提是我们得设计的时候为缓存的数据加上过期时间,而且呢,我们最终也发现,无论我们怎么操作,我们写数据库完以后写缓存。缓存里边更新,我们再来读数据,我们读到的最新数据肯定跟数据库的那一刻,刚存的最新数据有一段延迟时间,也就是说这一段延迟时间看大家能容忍有多大,我能容忍十秒内的延迟,十秒以后我必须看到新数据,还是我能容忍一毫秒,我就立即要看到新数据,还是我能容忍一天。
12:01
无论我们怎么容忍,我们都可以将这种成为我们叫最终一致性,就是我们数据库改后的值到我们最终看到的值,它们之间呢有一个比较大的延迟时间,但无论怎么延迟,我们最终都会看到数据库最新修改的值,所以呢,我们缓存数据的一致性就是完全满足我们最终一致性的,我们无论怎么设计,我们都保证我们最终缓存里边读到的数据就是数据库里边最新的数据,这是我们说的双写模式容易出现的问题。同样的失效模式呢,也会有这些问题,比如我们来举一个例子,那现在使用失效模式,那这个数据更新了以后写数据库,我们先来执行数据库,写成功了以后呢,我们再来删除缓存,那么我们删除缓存了以后,缓存中就没有数据了,下次再来查这个数据,数据缓存中没有,我们就会主动查数据库,主动更新缓存,像当我们触发了主动更新功能,我们最终得到最新的数据,但其实是这样来参考一个这个场景,现在呢是三个请求,这三个请求呢,顺序是这样子的,首先第一个请求还是比如我们来改一号的菜单数据,他这个数据呢改为一,改完以后呢,他删缓存,他执行完了,然后接下来第二个请求进来,他将我们这个数据呢改为二,但是呢,可能由于第二个机器先负载比较重,他做的事儿比较慢,然后呢,又来了一个第三个请求,比如他第三个请求在三号机器。
13:36
第一个请求在一号机器,第二个请求在二号机器,二号机器呢比较慢,他想把这个一号数据改成二。但是呢,它的操作比较慢,他花的时间比较长,那现在呢,出现这么一个问题,当第一个请求缓存删完了以后,第三个请求进来读缓存里边确实没数据,然后呢,他就去读数据库,但数据库呢,此时把我们这个二号数据还没改完嘞,像数据库还没提交我们最新的修改,那么呢,我们现在三号就读到了老的数据,这是一,然后呢,三号读完一以后,他要更新缓存。
14:12
但这块呢,又是一个关键点,如果说它的更新缓存运行的比较快,还好他更了一个错数据,然后我们写操作又把缓存删了,相当于他没更新,但如果他更新的比较慢,三号机器呢,走在这儿也墨迹了一下,然后呢,让二号机器数据库写完,立马把缓存给删了,相当于删缓存方法呢提前运行了。那我们这个更新数据呢,就没有任何人可以阻拦了,那我们把这个之前读到的一咔,我们就放到了缓存里边,所以呢,我们发现我们最新的数据二,我们还是放不到缓存,我们放的是数据一,还是我们说的脏数据问题,但这个问题呢,我们发现这是一个写和读的并发问题,那这个问题解决还是一样,我们所有的乱序问题都可以加锁来解决,这肯定没什么问题,但是呢,加了锁以后,系统呢,比较笨重一点。
15:06
所以现在就考虑另外一个场景,如果我们这个数据经常修改的话,我们还要不要放缓存,如果经常数据修改,为了保证我们缓存里边的数据一致性,为未修改数据和读数据,我读缓存的整个操作,那我们都加了锁,当然我们会发现我们数据经常修改,我们这个锁经常在,那我们整个系统呢就很慢,还不如不加锁直接查数据库,所以呢一点,我们这个经常修改的数据,我们想要实时的读取,而且实时性要求高的,那我们就应该直接读数据库。所以无论是我们这个失效模式还是双写模式,好的一点的设计就是我们缓存所有的数据我们都加了过期时间,哪怕暂时的脏数据,只要这个过期时间一到,把它一清,那我们又能得到我们最新的数据,相当于我们数据只要一稳定以后。
16:00
我们这个数据呢就是正确的,所以这两个模式无论用哪个都行,但是如果我们真的要精细的解决我们整个缓存一致性问题,那么给大家说一下我们的解决方案,那么看我们之前无论是我们双写还是失效模式,那都会导致我们缓存的不一致问题,我们给大家呢也分析了,要说我们多个实例,比如这个一号机器,二号机器,三号机器,有毒的,有写的,那么这个多个实例同时操作,能出现缓存数据不一致怎么办?那先来考虑我们这几个场景特点,第一个场景如果是我们用户维度的数据。比如我们用户的订单修改读取,包括用户的比如个人信息,他的签名啊之类的修改读取,这种并发几率非常小,有些同学就很奇怪,这个用户的访问量不是很大吗?怎么叫并发几率小啊?是访问量是很大,但是呢,一个用户他能不能一秒同时提交十个或者100万个修改他的个性签名呢?或者用户会不会一秒内同时提?
17:07
对于订单的两种不同操作呢,我们一会儿要退单,一会儿删单等等等等,所以我们这个呢,本来用户维度的这些数据,因为用户是通过我们这个手机APP或者电脑网站操作的。这手点鼠标也是很慢的。所以呢,我们这种并发几率呢比较小,我们完全可以不去考虑缓存不一致的问题,我们将这些数据确实加缓存都是没啥问题的,即使你害怕缓存不一致,我们给这些数据呢,加上过期时间,一段时间以后呢,我们让它触发主动的更新,也就是缓存过期了,我们将缓存删掉了,他主动再去查数据库更新就行,这是我们第一个场景,第二个场景如果是我们这个菜单品牌商品。人文系统里边天天用到我们的三级分类菜单,各种品牌,包括各种的商品介绍这些基础数据,这基础数据呢,发现一个最大的特点就是我们能容忍大程度的缓存不一致,什么叫大程度的?比如我们这个iPhone发布了,这个IPHONE11肯定京东呢第一时间上架了,那上架了以后呢,对于iPhone商品的介绍,我们第一时间在京东看的是一个样子,但是呢,官方将商品的介绍稍微修改了一下,京东也在后台修改了,我们也不知道京东是在后台哪一刻修改了,但我们可能三天、五天乃至十天以后我发现,诶,这iPhone的介绍怎么变了呢?
18:31
但这对我们业务并不能产生大影响,所以说呢,这些数据我们可以允许它的缓存的不一致,哪怕你存的是之前的旧数据也是可以的,所以呢,如果是这类的数据,我们也可以不用去考虑。当然想要考虑呢,还可以使用我们一个技术叫canon,那我们一会儿再说,还有我们这个第三个场景,我们现在业务设计呢,所有的缓存数据我们都加了过期时间,那这样呢,这个缓存的数据总会在某一刻触发它的主动更新,因为缓存本身就保证的是最终一致性,不是保证我们强一致,实时一致,包括在最后我们设计的时候。
19:13
我们可以通过加锁,而且这个枷锁呢,我们不要加我们以前的大锁,我们加读写锁。因为读写锁我们说最大的特点就是如果是写写状态下,那么写写排队,因为一有写锁,我们也在red之前看过,里边有个锁状胎位,但是呢,邪锁只要一释放,大家都用毒锁的话,毒索相当于无锁,因为大家都是并发毒的情况下,那没啥事儿都去来读,但是如果是并发写,那排好队,那巧了,一写一读,那正是我们读写所的特点,写的时候我们写所没释放,读就不能进行,比如来到我们这个场景,只要我们这个写锁不释放,你想在这一块读完成你的整个读流程没门。所以呢,我们可以在这些模式上,我们来加上读写锁,我们如果做了以上的东西,这种场景不考虑这种场景也不考虑这种场景,我们就要用缓存加过期时间。
20:10
那我们额外最多设计的时候再加一个读写锁。而且如果我们这个业务说我不关心这些脏数据,就是一些商品介绍,就是一些菜单,我系统更新了,哪怕三天以后看到最新结果,网站发生了变化都行,那我们连读写锁都不用加,所以这是我们说的缓存一致性的一些解决方案,那真正的解决方案就是你加上过期时间,你再来加读写锁,那后来我们再来介绍一个使用canon,但我们现在的场景,我们本身缓存的这些数据就是实时性一致性要求不高的,所以呢,我们都不需要考虑这种大量的过度设计,所以总结起来。我们实时性一致性要求高的,我们就去数据库查,实时性一致性要求不高的,读多写少的场景,我们就放到缓存中,害怕出现脏数据,加上读写锁,这是我们说的这个场景,包括我们还可以使用另外一个完美的解决我们缓存的不一致问题。
21:09
缓存能力不一致,大多是这样子的。我们数据库的最新更新,无论是写读状态也好,还是写写状态也好,数据库的最后一次更新没有映射到red上。由于这些线程乱了,它最后一次更新可能没有映射到red上,那么就可以使用我们这个canon canon呢,我们是这么来用的,比如我们使用canon来更新缓存,Canon是一个什么?它是阿里开源的一个中间件,这个中间件呢可以模拟,它是一个我们数据库的存服务器,比如我们这个MYSQL,我们有一个库。如果我们装了canel canel呢,就把它自己伪装成一个MYSQ的从服务器,那从服务器的特点就是MYSQL里边只要有什么变化,它都会同步过来,那么正好利用cann的这个特性,只要我们业务代码更新了数据库,我们马SQ这个数据库呢,我们肯定得开启我们blo这个二进制日志,日志里边呢,有每一次MYSQL什么东西更新了,什么东西更新了,Canon呢,就假装成MYSQL的一个存库,把MYSQ的每一个更新他都拿过来,相当于MYSQL只要有更新他就知道了,那他知道了MYSQL什么东西更新了,比如他看到了MYSQ菜单数据更新了,那他就去把red里边跟菜单有关的所有缓存,它就改掉就行了。而且用这种的好处就是我们在编码期间,我们只需要在这儿改数据库就行了,比如像在这儿,我们在这儿改完数据库就行了,我们也不要管缓存的任何操作,我们看在后台只要看数据库改了,把相应的缓存都改掉,所以呢,屏蔽了我们。
22:46
整个对缓存的操作,但是它的缺点呢,就是我们又加了一个中介件,还得额外开发一些我们自定义的功能,但这个的好处就是一次开发成型,我们以后呢就不管这些事儿了,看呢可以用来更新缓存。
23:01
开通,其实呢,在大数据情况下,更是用来解决我们整个数据易购问题,比如我们举一个例子,大家去京东,每一个人京东的首页都不同,首页里边推荐的商品都不一样,这都是基于你的爱好,你经常喜欢看数码产品,那就推荐的数码产品多,你经常喜欢买衣服,那就推荐的衣服多,所以这些东西呢,我们也可以用canon来做,比如canon可以怎么做,我们假设呢,我们每一个我们浏览的商品,我们数据库里边都有一个哪些人浏览了哪些商品的商品记录表,包括呢,我们数据库里边也有一些商品信息表。更或于我们购物车里边,我们购物车的数据有购物车相关的表,只要我们给购物车里边添了什么,我们浏览了什么,我们都知道,但我们现在想要做一件事,就是我们一进京东首页,给我推荐一些与我相关的,我感兴趣的商品,那怎么做呢?我们就可以使用canon canon呢,他来实时的订阅们访问记录表的一些更新,包括呢,也订阅我们商品信息的一些更新,想当canon就知道有哪些访问记录,有哪些商品信息,然后呢,他通过这些东西做一个分析计算,生成另外一张表叫用户推荐表。
24:16
他能把你每天的这个访问记录拿来给你进行计算,计算算出你感兴趣的商品,给你得到一个商品信息推荐表,那一来我们商城的网站首页,那想要看我们推荐的这些商品,我们只需要去这个推荐信息表里边儿去来查询数据就行了,我们不需要做我们这些大量计算,把我们的这些记录信息都拿来整一堆,所以呢,这是我们使用can可以来完成整个数据异构问题,就是不同架构的数据,我们想要把它们组装使用起来,我们也可以来解决。但在我们的业务系统里边,我们现在的缓存就是缓存这些读多写少的数据,而且呢,我们一致性要求不高的数据,我就不用加的这么复杂了,那未来我们需要给我们电商项目上大数据相关的系统的时候,我们可以来再来加上这些方案,那我们现在的方案呢,我就来采用我们的失效模式。
25:13
我写完数据库,我把直接把它相关的缓存一删就行了,让他自己来触发主动更新,那我害怕这个更新出现一些乱序问题,我来给它加上分布式的读写锁,那这就是我们最终的整个解决方案,在我们这个系统里边,我们系统的解决好我们我们系统的。一致性解决方案,所有的解决方案肯定都得跟着自己的系统来分析,得到最佳的解决方案,我们的解决方案呢,保证两点,第一点缓存的所有数据,所有数据都有过期时间,都有过期时间,那这样呢。即使有脏数据,我们也能触发主动更新,那么数据过期呢?来触发主动更新。
26:01
我们下一次查询就能触发主动更新,然后呢,接下来第二个我们还保证读写数据的时候。我们来加上分布式的读写锁,那有些同学说这对系统的性能会不会有极大的影响呢?如果是我们经常写,还经常读,这肯定有极大的影响,经常写经常读,我们肯定有极大的影响,但如果我们就偶尔写一次,读好多次,一点影响都没有,因为我们这个读写所,它最大的图特点就是。毒相当于无锁状态,只有在有写锁的情况下,那这个毒才去等待,所以就相当于我们后台管理员把我们这个数据库真的一修改以后,我们前端呢,用户先得等上三五秒,可能用户这次正在读,那我这次再修改,那你就得等上三五秒以后,我整个改完了你才能读到,那相当于只有这么一段时间,三到五秒的慢时间,只要这三五秒过去了,缓存中一有数据了,而且是最新数据了,那就跟以前不加锁是一模一样的,所以呢,我们最终使用读写锁来完成这个效果。
27:20
而且要用分布式读写锁,因为我们这些看到的就是一号实力,二号实力,三号实力,各种不同的机器模拟,有些机器快,有些机器慢而产生的问题,所以我们一定要锁住所有的机器,让他们按照顺序,一旦有修改,因为我们修改的并发很小,就是管理员后台改一下就一个请求,最多两个,三个、五个,所以我们让他排好队,这是我们说的缓存数据一致性的解决方案,那我们最终就用这个。好,我们下一节课呢,就来改造我们的业务代码使用上我们这些缓存一致性保证的这个时效模式。
我来说两句