1、unlock() 释放锁源码
可重入锁 RedissonLock 释放锁的源码还是比较简单的,我们可以分为两步:第一步是执行释放锁的lua脚本,第二步就是停止 watchdog 的运行。
RedissonLock#unlockInnerAsync:
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
// 停止 watchdog 的定时续过期时间,其实就是将对应的 ExpirationEntry 从 EXPIRATION_RENEWAL_MAP 中移除,当 watchdog 执行时发现当前客户端当前线程没有 ExpirationEntry 了,那么就会停止执行了。
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
// 如果 lua脚本返回的是null,证明当前线程之前并没有成功获取锁,执行tryFailure方法
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
// 成功释放锁,执行 trySuccess 方法
result.trySuccess(null);
});
return result;
}
2、释放锁 lua 脚本
上面我们看到,释放锁的源码还是比较少的,我们也看到大概就分为几个步骤,但其实最重要的释放锁逻辑在于 lua 脚本,所以下面我们将一个步骤一个步骤去分析。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
2.1、KEYS
Arrays.asList(getName(), getChannelName())
KEYS:[“myLock”,“redisson_lock__channel:{myLock}”]
2.2、ARGVS
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)
ARGVS:[0,30000,“UUID:threadId”]
2.3、lua 脚本分析
1、分支一:当前线程没有加过锁
lua 脚本:
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
分析:
- 利用 hexists 判断是否为当线程加锁
hexists myLock UUID:threadId
- 加锁记录不存在,则直接返回nil,即null,表示释放锁失败,终止lua脚本执行
2、当前线程加过锁,扣减加锁次数
lua脚本:
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
分析:
- 利用 hincrby 命令扣减加锁次数
hincrby myLock UUID:threadId -1
3、分支二:当前线程加锁次数大于1
场景:
- 当前线程持有锁数量大于1
- 当释放一次锁后,需刷新锁的过期时间,不用删除锁
lua 脚本:
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
分析:
-
剩余持有锁次数大于0,利用 pexpire 命令刷新锁过期时间
pexpire myLock 30000
-
返回0,表示释放锁成功,终止lua脚本的执行
4、分支三:当前线程仅持有一次锁
场景:
- 当前线程仅持有一次锁
- 当释放锁后,需删除对应锁 key,并往channel发送释放锁消息,通知其他线程可以获取锁了
lua脚本:
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
分析:
-
利用 del 删除锁key
del myLock
-
往锁对应channel发送释放锁消息
publish redisson_lock__channel:{myLock} 0
其实就是我们上篇说的,如果获取锁失败,再等待的时候,同时会订阅一个channel,当接收到channel有消息,那么证明有客户端释放锁了, 就可以从等待状态中醒过来,重新尝试获取锁。这样可以避免不必要的等待。