Redission可重入锁原理
Redission
Redission是一个在 Redis 的基础上实现的 java 驻内存数据网络,它提供了一系列分布式 Java 对象和许多分布式服务。
可重入:
同一个线程可以多次获取同一把锁
问题引出:
调用一个方法时,它首先给自己上了锁,然后执行业务逻辑过程中,它会调用另一个方法,而另一个方法也会先去获取同一把锁,而此时前一个方法正拿着这把锁不放,这就会造成死锁问题,而为了能够让后执行的方法得到锁,这就需要使用可重入锁。
原理图:
在使用trylock的时候,Redisson会在Redis中创建一个Hash结构的键值对,它的键是我们自定义的键,而值是随着我们调用tryLock的次数递增的,一个方法获取了锁,先创建key,然后让value+1,如果方法内又调用了其他方法,同时该方法也去获取锁,在判断完锁是当前线程的之后,它也会让value+1,如果锁不是当前线程的锁,那么就会直接返回。如果要释放锁,它也会先判断锁是否是当前线程的,若是,则让value-1(注意,这里没有使用del key来删除锁),若不是,则直接返回,一旦value=0之后,就说明全部的锁被释放,那么就可以正式地释放锁,也就是调用del key 来释放锁。
获取锁源码
在 Redission 中,其通过 trylock
方法 来获取锁,其底层是通过调用一个 tryLockAsync()
实现:
public boolean tryLock() {
return (Boolean)this.get(this.tryLockAsync());
}
追踪 tryLockAsync() 方法
我们可以看到,它获取了当前线程ID然后执行另一个tryLockAsync(long threadId)
方法:
这里它调用了 this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId)
方法,并设置了默认的等待时间和释放时间为 -1L:
这是 tryAcquireOnceAsync 的具体实现:
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
方法定义:
- 这是一个私有方法,返回一个
RFuture<Boolean>
对象,表示尝试获取锁的异步结果。 - 方法接受四个参数:
waitTime
表示等待时间,leaseTime
表示锁的持有时间,unit
表示时间单位,threadId
表示线程的ID。
对于条件判断的内容:
- 首先,检查
leaseTime
是否等于-1L。如果不等于-1L,表示有指定的锁超时时间,将调用tryLockInnerAsync
方法来尝试获取锁,传递相应的参数。 - 如果
leaseTime
等于-1L,表示没有指定锁的持有时间,将调用tryLockInnerAsync
方法来尝试获取锁,但是将锁的超时时间设置为Redisson配置中的lockWatchdogTimeout
,并传递相应的参数。
- 默认的超时时间为 30000 毫秒
- 无论前面的哪种情况,都会返回一个
ttlRemainingFuture
对象,表示锁的剩余生存时间。 - 在
ttlRemainingFuture
对象的完成回调方法中,判断如果没有发生异常且ttlRemaining
为true
,则调用scheduleExpirationRenewal
方法来安排锁的过期续约。
由于我们设置的超时释放时间为 -1L,所以第一个判断语句不会执行。
第二行它执行了 tryLockInnerAsync
的方法
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}
咋们来看一下中间的这一串 lua 脚本:
if (redis.call('exists', KEYS[1]) == 0)
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
以及它对应的三个参数:
- Collections.singletonList(this.getName()) 这个参数表示一个只包含一个元素的不可变列表,元素是通过
this.getName()
方法获取的锁的名称 - this.internalLockLeaseTime :锁的超时释放时间,以毫秒为单位
- this.getLockName(threadId) :表示通过
threadId
获取的锁的名称
我们来解释一下上面的 lua 脚本的含义:
- 首先,判断锁是否存在,如果不存在,则通过
hincrby
命令将锁的计数器加一,并刷新锁的过期时间,然后返回nil
。 - 如果锁已存在,且当前线程已经持有该锁,则同样通过
hincrby
命令将锁的计数器加一,并刷新锁的过期时间,然后返回nil
。 - 如果锁已存在,但当前线程未持有该锁(说明锁不是当前线程的),则返回锁的剩余生存时间。
这就是 Redission 可重入锁获取锁的实现,它通过判断当前获取锁的线程是否和 Redis 保存的锁的线程信息一致。若是,则获取锁成功,可以继续往下执行业务,若不是,则直接返回,无法获取锁。
释放锁源码
通过追踪 Redission 的 unlock()
方法:
可以看到,其执行了 unlockAsync
方法,并传入了当前线程ID:
进入 unlockAsync(long threadId)
方法,可以看到类似于获取锁,它也执行了一个 unlockInnerAsync(threadId)
的方法:
该方法的实现也是通过一个 lua 脚本实现的释放锁逻辑:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
}
lua脚本内容:
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil
- 首先,判断锁是否存在,如果不存在,则直接返回
nil
。 - 如果锁存在,则通过
hincrby
命令将锁的计数器减一,并获取计数器的值。- 如果计数器大于0,则表示仍有其他线程持有该锁,通过
pexpire
命令续约锁的过期时间,并返回0。 - 如果计数器等于0,则表示当前线程是最后一个持有锁的线程,通过
del
命令删除锁,并通过publish
命令发布一个解锁消息,返回1。
- 如果计数器大于0,则表示仍有其他线程持有该锁,通过
- 如果锁不存在或其他异常情况,则返回
nil
。
rby` 命令将锁的计数器减一,并获取计数器的值。
- 如果计数器大于0,则表示仍有其他线程持有该锁,通过
pexpire
命令续约锁的过期时间,并返回0。 - 如果计数器等于0,则表示当前线程是最后一个持有锁的线程,通过
del
命令删除锁,并通过publish
命令发布一个解锁消息,返回1。 - 如果锁不存在或其他异常情况,则返回
nil
。
总之,释放锁是通过减少锁的计数器来实现锁的释放,并根据计数器的值判断是否需要续约或者删除锁。如果当前线程是最后一个持有锁的线程,则会发布一个解锁消息。