一、分析问题
在刚刚我们已经实现了基于redis互斥锁的一个初级版本,在这个版本中我们采用了setnx和ex的这种方式来实现互斥;在释放锁的时候采用的是del,直接将锁删掉,这样其他人就能去获取锁了,实现方式非常简单。
在大多数情况下,这个锁都能正常的工作。但是在一些极端情况下,它依然会存在一些问题,所以这节课我们就来分析一下它可能存在什么样的问题。
这里准备了一条线,可以将它理解成redis锁持有的一个周期。
接着假设来了一个线程1,线程1在尝试的过程中首先需要去获取锁,此时它就会像redis发送请求获取锁。因为它是第一个来的,所以它能正确的获取锁,没有人去阻拦它,这里使用蓝色的线来标识线程一拿到了锁。
拿到锁后它就要开始执行自己的业务了,但是因为某种原因它的业务产生了阻塞,这样一来它的锁的持有周期就会变长,到什么时候为止呢?
- 情况一:它执行完了,然后它去释放
- 情况二:它阻塞时间太长了,甚至超过了我们设置的那个超时时间,也就是说业务还没完,它就超时释放了
那么它一旦提前释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁(这里使用紫色标识),然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况。
但是线程2并不知道,线程2还在执行自己的业务,就在这时线程3来了,也来获取锁(这里用棕色标记),结果由于线程2的锁被线程1删了,此时线程3也能获取锁成功,然后开始执行自己的业务
想想看,此时此刻就同时有两个线程都拿到了锁,都在执行业务,所以又一次出现了这种并行执行的情况,因此线程线程安全的问题就有可能再次发生,这就是所谓的极端情况。
最后来分析一下这种极端情况产生的原因是什么?首先是因为业务阻塞导致了锁提前释放。然后当线程1醒过来后,这个时候的锁已经不是线程1的锁,而是线程2的锁,但是线程1二话不说上来就将别人的锁给删了。因此这里发生线程安全问题的最重要的原因就是:线程1在释放锁的时候,它把别人的锁删了。
二、解决问题
解决方案:解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。
当初我们在获取锁的时候存了一个线程标识进去,例如下面存的43就是线程的id
如果说我在删除锁的时候,将锁的线程标识取出来,判断一下跟我当前的线程是否一样,就可以避免这个问题了。
如果不一样,例如线程1想要去释放锁,发现这个锁是紫色的,但是线程1自己的锁是蓝色的,此时线程1就什么都不做,这样就可以避免释放别人的锁。
同样的道理线程2也是,它在执行自己业务的时候,因为线程1没有删它的锁,这样将来的锁依然存在,因此线程2就可以不受干扰的去执行了,就不会存在刚才的那种情况了。
当线程2执行完业务后,它去释放锁的时候也要做这个判断锁的动作,此时这个锁依然是紫色,标识一样,返回ok,此时它就可以正常的去释放锁了,然后向redis发一次请求去释放锁,此时它的锁就被释放掉了。
此时线程3才能去获取锁,去执行自己的业务,这样一来就避免了问题的发生了。
三、总结
解决这个问题的关键就是在释放锁的时候做一个判断,因此业务流程与原来业务相比就会有些变化了