Redisson分布式锁的使用及部分底层原理
lock与tryLock
项目中我们使用Redisson做分布式锁,最常见的一种用法如下:
class Test{
private final String PUBLIC_KEY = "bbe:credit:user:2020122212450";
public void test(){
RLock lock = redissonClient.getLock(PUBLIC_KEY);
lock.lock();
try {
//业务代码
}finally {
lock.unlock();
}
}
}
加锁后,在finally中解锁,锁必定在业务代码执行或异常后被释放,这一切都看起来都非常理所应当。
然而分布式锁和传统的多线程编程最大的区别在于多实例,当一个实例持有锁,在业务代码执行到一半的时候宕机了,从代码机制层面我们保证这个锁必定会被释放,然而宕机不会管这些,
在这种情况下会发生什么情况?会造成其他实例一直获取不到锁吗?这篇文章主要就是讨论这个问题。
我们先来看它另一个函数tryLock的用法:
class Test{
private final String PUBLIC_KEY = "bbe:credit:user:2020122212450";
public void test(){
RLock lock = redissonClient.getLock(PUBLIC_KEY);
try{
boolean locked = lock.tryLock(30, 30, TimeUnit.SECONDS);
if (!locked){
//获取锁失败,补偿处理
return;//获取锁失败,不继续执行业务流程
}
}catch (InterruptedException e){
//线程中断,异常提示
}
try{
//业务代码
}finally{
lock.unlock();
}
}
}
java文档(RLock接口):
/**
* Tries to acquire the lock with defined <code>leaseTime</code>.
* Waits up to defined <code>waitTime</code> if necessary until the lock became available.
*
* Lock will be released automatically after defined <code>leaseTime</code> interval.
*
* @param waitTime the maximum time to acquire the lock
* @param leaseTime lease time
* @param unit time unit
* @return <code>true</code> if lock is successfully acquired,
* otherwise <code>false</code> if lock is already set.
* @throws InterruptedException - if the thread is interrupted
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
tryLock首先会尝试获得锁,当有其他线程正在占用锁时,它会对当前线程进行阻塞,在waitTime单位时长内,不断的尝试去获取锁,直到获取到锁为止。
获取到则返回true,逾期未获取到则返回false。而第二个参数leaseTime可以设置key的过期时长,若在leaseTime单位时间内,我们没有主动调用unlock方法,
则锁在过期后直接释放。
我们再观察上述示例代码,因为上述特性,在使用tryLock时,我们要对tryLock的返回结果进行判断,并根据获取锁的情况来决定是否要继续执行业务代码。而由于
tryLock会阻塞线程,为了防止一些特殊情况,比如其他线程向当前线程发送了中断信号,线程的阻塞被打破了,所以我们要捕获InterruptedException并进行补偿处理。
而正常情况下,在执行业务代码之后,需要主动将锁释放。
这里可能有人要问了,既然设定了leaseTime会自动释放锁,那么还有必要再unlock一次吗?我的看法是有必要的,因为实际上我们并不能确定我们的业务代码会执行多久,
如果设置短了,过期时若业务代码还没执行完,其他进程获取到了锁,就会有两个线程同时在执行业务代码,这肯定不是我们想要看到的。实际上我建议将leaseTime设置为-1,
此时key的过期时间为“无限”,只要我们没有手动释放,就会被线程一直持有。
为什么“无限”要加上引号,因为这个无限并不是指在redis中这个key没有被设置过期时间,而是基于redisson内部机制自动刷新的,这就是我们下面要介绍的内容。
leaseTime与watchDog
我们来看lock的源码。
可以看到lock默认是将leaseTime设置为-1的,而继续追踪源码我们可以看到这么一个判断:
可以发现,leaseTime是-1与否,其实调用的是同一个函数,区别就在于传参,以及增加了一个回调函数。
当leaseTime为-1的时候,传入的leaseTime为30000,这个默认参数位于Redisson的Config类中,这点我们通过追踪源码很快就能确定。
再来看这个tryLockInnerAsync函数,里面执行了一段lua脚本,这个就是加锁的核心逻辑了。之所以用lua脚本的方式执行加锁,是因为这样能保证操作的原子性。
通过阅读这段lua脚本,我们可以得知,如果获取锁成功或锁重入时,脚本将会返回空,如果获取锁失败,将会返回锁的剩余时间
而下面的回调,就是watchDog的核心机制所在。
追踪源码,我们可以看到这样一段代码
来了,我们知道,leaseTime为-1的时候,实际的leaseTime是30s,因此每过10s,redisson就会自动重置一次锁过期时间。
思考:为什么要这样做呢?在这种机制下,它保证了在宕机情况下,锁会被自动释放。实例本身宕机的情况下,若锁一直不释放,之后其他实例如果想要进行重试都会被阻塞在这个锁上,这对生产来说
是灾难性质的,而一个已经挂掉的实例持有锁毫无意义。
Redisson锁的结构
Redisson使用Hash作为锁在redis中的存储结构,并以此实现可重入锁
key是我们自定义的,即是getLock的入参。当线程A获取到锁时,会在key对应的HASH结构里新增一对KV,redisson会使用自身ConnectionManager的id(是一个UUID,通过UUID.randomUUID()生成)+“:”+线程编号作为key值,value则是
锁进入的次数,当同一个线程多次重入锁时,value值会依次+1,而对应的,每次unlock都会减一,只有当value值为0时,这个锁才会被释放。
结论
根据上文可知,waitTime和leaseTime在redisson锁中是两个非常重要的参数,默认情况下两者都是-1,而leaseTime为-1时,实际上是启用了看门狗机制,会定期刷新锁的过期时间。
我们应当根据不同的业务需要来进行这两者的设置。