开发者学堂课程【高校精品课-厦门大学 -JavaEE 平台技术:用户重复登录】学习笔记,与课程紧密联系,让用户快速学习知识
课程地址:https://developer.aliyun.com/learning/course/80/detail/15943
用户重复登录
首先讲解一下上次重构的代码,里面最主要的重构部分、主要修改的代码 login 如下所示。在其他社会层的代码中间都是空的,因为到目前为止,写的所有的代码都是正常改查,所以在收拾代码上基本无内容。但是该登录代码,在社会层是有内容的,因为其登录逻辑是在社会层上完成的。
单纯从登录来说,是相对比较简单的,所以重点要讲的是关于控制唯一登陆的问
题。在上次课中,做代码 reveal 时,当时提交的代码又打回去了,主要原因是当时没有做唯一登陆的控制。那么怎样做唯一登陆的控制呢?
逻辑为当同一个用户名第二次登录时,就会将第一次登录的踢出,即第一次登录的用户在之后,再也不能访问系统了。踢出去的方法用的是 JWT,原先 JWT 生成时,对 createtoken 代码做了修改,原来生成时已经加了 expireTime,一般来说,上一次登录和下一次登录总是有时间间隔的。但是在测试时发现不行,因为第一、二次登录算出来的 JWT 是完全一样的,是没有办法区分上一次登录和这次登录的。原因 expireTime 算的时间是以秒记的,而在测试时非常快,一秒钟之内就登录了两次,所以造成两次的 JWT 是一样的。现在将代码中间加了序号,每一个 token
发出去时,token 本身是有序号的,且序号每次都不一样,所以这样就会知道每一次用户,即使是相同用户名登录进来的 token 都是不一样的。
有了这样的前提,做的方式就是在代码中间,定义了一个集合,上图所示中间一段。这一段需要判断如果有重复登录的,就需要将原来登录的 JWT 拿出来。
其实每一个用户都把登录的 JWT 存进去了,所以将其拿出来,要用 logger.debug("login :old7wt*. jut); this.ban2nt(jwt);方法将登录的 JWT 禁止。做法是定义了一个集合,该集合叫做 BanJwt,将所有需要禁止登录的 JWT 丢到集合里,所以网关在登录时,不仅要检查用户名、token 是否正确,还要看 token 是否被禁止。如果 token 被禁止,就不让其登录,用这样的方式将前面的登录用户踢出去。但是该问题没有想象的简单,如果简单的将 JWT 丢到里面,会存在一个问题,即集合如果只进不出的话,会越来越大,所以还要想办法如何让集合出。因为 JWT 是有有效期的,所以丢进去集合的 JWT,检查有限期是否过期,从集合里拿掉,这是想到的相对较简单的办法。这样的办法其实是不行的,因为要去判断有效期的话,意味着要去扫描该集合,一旦要扫描集合,都知道这是要尽量避免的事情,因
为太消耗 cpu 的资源了。
所以在设计时,其实做了两个集合,做了一个 BanJwt0 和 BanJwt_1,两个集合都设置了同样的有效期,为集合在 redis 中存在的时间,是两倍 Jwt 的过期时间 Expiretime。因为每个阶段都有过期时间,网关也会检查如果 JWT 过期了,也不会让其登录。所以集合设置了两倍 jwt 的过期时间,是为了在一半时切换集合,比如开始用的为0,不停地往其中丢要禁止的 jwt,到了一半的时间时,就不往0的里面去丢了,改到往1里面丢。丢到1的一半时,意味着0的集合里所有的 jwt 都过期了,至少之前一半位置丢进去的都过期了,那就将0集合删掉,再重新往0里丢。不删掉的话,有效期已经过期了,0集合都不要了,再重新往0前一半集合里丢。如果该集合丢到一半,即下面的集合也都全部过期了,所以再将下面的丢掉,再往里面
丢。
这样就是用两个集合保证丢进去的 jwt 是会消除掉的,要把缓存之外的丢,不往外面拿的话,缓存总有一天会爆掉的。所以,这是初始的设计思想,来自于 jvm 垃圾收集的机制,有兴趣的可以去看一看,Jvm 的垃圾收集机制和该思想非常像。
这样做了后,带来一个问题,即需要知道当前往哪一个集合中丢,所以做了第三个
在 redis 中间的项,叫做 BanIndex,是一个一直往上加的整数。不写0或1的原因是不太好做,若是0就需要切成1,是1的话要切成0,需要做判断,是0的话就是1,是1的话就是0,这样就需要两句。这在后面会提到,这是一个互斥的过程,操作系统中很重要的一个概念“互斥的过程”,其有一个原则为尽量使用原值性操作,但凡有两条语句,在两条语句执行中间都有可能被另外一个用户插进去。所以最好用一句去解决,因为一句是原值性的,BanIndex 为了让其一句句解决,就不停的向上加,从0一直向上加。要知道当前是0还是1,看其摩尔,因为摩尔要么是0要么是1,用摩尔的方式做处理。所以,每次切换后,切的为 BanIndex 的值加1,第二次
要访问的为 BanJwt0 加上该数的值,0的值是摩尔的,就知道当前是什么了。
所以以上是用 BanIndex 做切换,这里可能觉得还比较简单,但是写了很多代码,
问题在于,BanIndex 的切换不是靠一句话完成的,下图为切换过程。共有四句,
第一句是切换
bannIndex=redisTemplate.cpsforvalut().incrument(’banIndex"),
用
原子器操作,该操作就是 banIndex 加1。
第二句话是
currentSethane=banSetiiame!(int)(bannindex% banSethane.Iength)1),
加1后需要知道当的banset 是谁。
第三句话是
redisTempIate.opsfarset().add(currentSethane;jwt);
将新的东西加到新的里面去,其实主要互斥的是前两句话。将三句话提出来,第一句是
ops.incrument(banIndex)
,第二句话是 CurrSetName=banJwt+banIndex
,这里写的是示意,第三句话为 add(curSexName,jwt)。
这三句话要做互斥,因为前两句话之间,第一句话如果都进行加1,用加1的得到当
前集合的名字,然后加到当前集合名里,这是没有问题的。如果有两个线程进来没有做互斥,意味着可能会出现,第一句做了加1,在做切换之前,第二个线程进来再做了加1。本来是0,变成了1,第一个线程是1,第二个线程进来时,如果在切换2xJwtExpire 上,同时有两个线程在该点过来,都需要加1,第一次加成的1,第二次进来就会加成2,其就变为0了。所以会发现第一个进程过来将其切成 BanJwt_1
的线程、集合,如果不做互斥的话,第二个进来又会切回到上面 BanJwt0,就会造成切换的失败。
所以,意味着刚刚三句代码是需要加互斥的,需要让任何时候只有一个线程能够执行这段代码。但是互斥不是操作系统上的互斥,操作系统上的互斥是指在同一台机器上这段代码只能让一个线程或一个进程进来,而现在的互斥是在整个服务器集群里,所有服务器集群、所有的线程里,只能有一个线程在执行这一段代码。所以这
里的控制范围比操作系统的控制范围更大,因为操作系统是单机的,这里是集群的,操作系统是单机上的一个线程,这里是在集群上所有机器、所有线程只能有一个线程进来做完这段,其他线程才能进去。
刚刚三段代码区在操作系统里有一个专有名词,叫做关键区 critical area。为了控制只有一个线程能进入关键区,操作系统中有另一个名词,叫做信号量。所以,需要用一个信号量来控制整个服务器集群上(不是单台服务器上)只有一个线程能进去到关键区。当然,这个信号量是互斥信号量0、1,不能允许多个线程进来,只能允许一个线程进来,所以其是互斥信号量。
在 redis 中如何做到这点?因为不是操作系统层面上的,所以无法用操作系统信号量的方式完成,范围是集群上所有服务器的线程都要在该关键区做互斥。Redis 用其中间的一个方法,叫做 setifabsent,即在其中定义的第四个值,叫做 banIndexlocker。该值叫什么名字并没有什么意义,该值作用是用来做互斥信号量,因为现在值互斥,所以不需要知道其值是多少,它可以做成信号量那样,比如3或4的信号量。现在因为只需要做互斥,所以用 banIndexlocker 做互斥,court 原值性操作 setifabsent。
该方法有一个很有趣的特征,即如果该值在缓存中没有,它的返回值是1,其不但会设定该值,而且返回值为1。如果在内存中有该值,setifabsent 返回的是0,即它不能设,利用其返回值的0、1,探测在 redis 中间存不存在 banIndexlocker 值,用该值做互斥信号量。换句话说,在关键区前面,判断该值有没有,若没有,说明没有任何代码在这段关键区里面进去执行,进去时就需要将值设进去,然后其他的线程过来后,会发现已经有该值了,就会被挡在关键区的外面,不让其进去。
判断其有没有和设置它,必须是其原值变量,在操作系统中应该专门有互斥变量,这里没有互斥变量,所以用 setifabsent 的操作判断其有没有和设置它的数做成 redis 原值操作。如果没锁的话,加锁,有锁的话,返回的就是 false,所以代码中的值应为 true,先不看第一个代码,true 的话就会进入到循环里,该循环主要目的是睡眠,重新看一下 BanIndex 值。因为这里的特征不是每一个线程都要进去做切换,目的是控制一个线程做切换,其他的线程不需要做切换,因为直接用新的即可。所以可以看到代码中还有另外一个判断条件,即 newInsdex=BanIndex,现在来说,BanIndex 是在前面如下所示这段。
redisTenplate.ogsforvalwe().set("banledex’y tong.valucof(9));
}else (
logger.detug("banfutibanindesa”aredisTemplatecopsforvalanD.gus(ftenindes");
banindex =Long.parseiong(redisTemplute.vesroryslun().gat(banIatex").tastring);
若没有的话,会直接设置成0,若有,会直接将 BanIndex 读出来,所以 BanIndex 当前已经读出来的值,newIndex 最开始和 BanIndex 一致。如果要修改,进来后第一个条件肯定是真的,第二个条件看是否能获得锁,若能获得锁,循环就不会进
去,直接到后面去改值了,因为第二条就是 false;若不能获得锁,就会在循环里,该循环不断去得到当前的值有没有改变,得到新的值,如果得出的值和之前的值不一样,说明被别的线程改变了,就可以退出等待,继续做后面的。
代码中加了判断,如果是被别的线程改变了,其实不需要做什么,将其变成新的线程在后面做动作。所以这里用 banIndexlocker 在 redis 中间实现了分布式锁,即
其范围不是单台服务器线程之间的互斥,而是在整个集群中所有服务器线程的互斥信号量,称之为分布式锁机制。
这个例子讲解的两个原因,其一是操作系统中学的内容这门课需要用到,因为是高并发的系统。这里面一定存在一个问题,即互斥怎么解决。如果高并发系统里两个难题,一个是不互斥时怎么样快,二是要互斥时,如何解决高并发的问题。所有互斥的场合和操作系统不一样,不是单台服务器,而是多台服务器上所有的线程。所以利用 redis 做分布式的锁是代码中经常会用到的技巧,这里也能看到,redis 不仅仅可以用来做缓存,其数据结构、某些函数的特性,可以用来做很多新的功能。在
整个单个服务器里面,有两个保障性的东西,一是 redis,二是消气服务器。
即 redis 不仅仅用来做缓存,而且可以做很多其他的功能,消气服务器不仅仅做异步和消封,还可以做很多奇妙的功能。所以,利用 redis 和消气服务器两个东西,就能够解决对单台服务器的高并发和大负载的问题,这也是利用代码解决高并发和
大负载的手段,一个是从代码上,一个是从体系结构上。
从代码上可以解决高并发和大负载的主要手段就是 redis 和消气服务器,其他的部
分并不是在代码上解决的,而是在配置上配置成多服务器的体系,解决高负载的问题,大并发的问题一定是有互斥的要去解决。