在Java中有很多保证线程安全的方式,比如synchorized,lock锁等等,这些在单机环境下都能发挥不错的作用,但是在分布式的环境下,这些机制就会失去大部分的作用。
在分布式环境下就需要引入分布式锁,实现分布式锁的方式有好多种,比如redis、zookeeper,或者通过数据库来实现,但是在分布式的情况下还需要考虑机器宕机的情况,如果某台机器上的线程获取到了这个锁,但此时机器宕机了。那么就没办法去释放,就会造成死锁的情况。
为了避免这种情况,就需要给锁加上一个过期时间,而过期时间的设定又是一个令人非常头疼的问题。
在Redisson种有一个看门狗机制,它给出了一种过期时间的很好的解决办法。下面就来研究一下它具体实现吧。
Redisson的加锁入口是tryLock(),此方法需提供获取锁的等待时间,如果在规定时间内未抢到锁,会返回false。
这里可以看到tryLock()方法实际上是调用了下面这个方法,这里给了一个leaseTime的默认值,至于为什么是-1,我们接着往下看。
进来之后会发现,这个方法的核心就是执行一个tryAcquire方法,我们点进去看一下。
tryAcquire方法实际会去执行tryAcquireAsync异步的去获取锁,然后再使用get获取结果,如果结果为null代表获取锁成功,这里后面会讲。
然后进到tryAcquireAsync方法,在这里判断了leaseTime是不是-1,如果我们自己设定了过期时间,那么就会以我们设置的为准,并且不会去开启自动续期。
如果是默认的-1,那么异步获取锁之后,后面还会去开启一个自动续期的定时任务。
异步获取锁是通过tryLockInnerAsync这个方法实现的。第一个参数是30000,传入的是commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),点进去可以看到
在这个方法里使用lua脚本的方式去执行了set操作
这段lua脚本的意思是:
锁不存在,加锁成功,设置hash数据结构锁: 锁名 -> 加锁线程:id -> 加锁次数(1)
锁存在且是本线程的锁 加锁次数增加:锁名 -> 加锁线程:id -> 加锁次数+1
锁存在且不是本线程的锁 加锁失败 返回锁剩余过期时间
从这里也可以体现出此锁的可重入,即某一线程获取到锁之后,那么这个线程再去获取该锁的话也可以成功
同时也可以如果返回为null那么说明获取锁成功。
然后后面会判断如果结果为null,就会去执行scheduleExpirationRenewal(threadId)方法,进去看一下
由于我们的Redission的分布式锁是可重入锁,所以这里会首先判断一下是不是第一次加锁,如果不是第一次则加锁次数加 1 不会再开启续期 因为第一次加锁时调用
如果是第一次加锁的话就回去调用renewExpiraton()去开启自动续期。
addThreadId:重入次数+1
renewExpiraton()开启自动续期这个方法里面创建了一个定时任务,主要逻辑是通过renewExpirationAsync(threadId)方法去执行续期逻辑,执行成功后还会通过下面if (res) {renewExpiration();}方法递归调用。
注意到这个线程执行的间隔是internalLockLeaseTime / 3,也就是30 / 3 = 10s
我们可以看一下renewExpirationAsync方法里面的逻辑
此lua脚本的意思是:当前线程持有的锁是否还存在 存在的话重新设置锁的过期时间(默认 30 秒)
至此加锁的逻辑就追完了。
下面我们看一看释放锁的逻辑。其入口为:unlock方法,它会去调用unlockAsync方法。
unlockAsync里面掉了unlockInnerAsync方法去释放锁,
unlockInnerAsync方法点进去我们可以看到它也是通过lua脚本的方式去释放锁。
若锁不存在 返回 若锁存在 加锁次数 -1 若加锁次数仍不等于 0 (可重入),重新设置锁的过期时间,返回 若加锁次数减为 0,删除锁,同步发布释放锁事件,返回