java
rotected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
Redisson 解锁 Lua 脚本详细解析
下面我将深入分析 Redisson 解锁的核心 Lua 脚本,这个脚本实现了分布式锁的释放逻辑,包括锁的可重入计数减少、锁的完全释放以及解锁通知机制。
脚本完整代码
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;
参数说明
-
KEYS[1]: 锁的名称(Redis key)
-
KEYS[2]: 锁的频道名称(用于发布解锁消息)
-
ARGV[1]: 解锁消息内容(固定为 0L)
-
ARGV[2]: 锁的过期时间(毫秒)
-
ARGV[3]: 锁的唯一标识(客户端ID + 线程ID)
逐行解析
第一部分:检查锁的持有权
lua
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-
redis.call('hexists', KEYS[1], ARGV[3]) == 0
-
检查锁是否由当前线程持有
-
hexists
命令检查 Hash 中是否存在指定字段 -
返回 0 表示当前线程不持有该锁
-
-
return nil
-
如果锁不是由当前线程持有,直接返回 nil
-
防止误删其他线程持有的锁
-
第二部分:减少重入计数
lua
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-
redis.call('hincrby', KEYS[1], ARGV[3], -1)
-
减少当前线程的锁持有计数(实现可重入锁的关键)
-
使用
hincrby
命令将字段值减 1 -
将结果存储在局部变量
counter
中
-
第三部分:处理重入计数仍大于0的情况
lua
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
end;
-
counter > 0
-
检查减少后的计数是否仍大于 0
-
表示当前线程仍然持有该锁(重入情况)
-
-
redis.call('pexpire', KEYS[1], ARGV[2])
-
更新锁的过期时间
-
保证锁在剩余持有期间不会过期
-
-
return 0
- 返回 0 表示锁未完全释放(只是减少了重入计数)
第四部分:完全释放锁
lua
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
-
redis.call('del', KEYS[1])
-
当计数减到 0 时,完全释放锁
-
删除 Redis 中的锁键
-
-
redis.call('publish', KEYS[2], ARGV[1])
-
向锁的频道发布解锁消息
-
通知其他等待获取锁的客户端
-
ARGV[1]
是固定消息内容(0L)
-
-
return 1
- 返回 1 表示锁已完全释放
第五部分:默认返回
lua
return nil;
-
理论上不会执行到这里
-
作为脚本的默认返回值
脚本执行流程总结
-
检查锁持有权 → 如果不是当前线程持有 → 返回 nil(解锁失败)
-
是当前线程持有 → 减少重入计数 →
-
如果计数仍>0 → 更新过期时间 → 返回 0(部分解锁)
-
如果计数=0 → 删除锁 → 发布解锁消息 → 返回 1(完全解锁)
-
关键设计点
-
线程安全验证:先验证锁的持有者,防止误删
-
可重入支持:通过计数机制实现
-
锁续期:部分解锁时更新过期时间
-
通知机制:完全释放时发布消息通知等待者
-
原子性操作:整个解锁过程在一个 Lua 脚本中完成
参数传递分析
Java 方法调用时的参数传递:
java
Arrays.asList(
getRawName(), // KEYS[1] - 锁名
getChannelName() // KEYS[2] - 频道名
),
LockPubSub.UNLOCK_MESSAGE, // ARGV[1] - 解锁消息(0L)
internalLockLeaseTime, // ARGV[2] - 锁过期时间
getLockName(threadId) // ARGV[3] - 锁持有者标识
实际应用场景示例
假设:
-
KEYS[1] = "my_lock"
-
KEYS[2] = "redisson_lock__channel:{my_lock}"
-
ARGV[1] = 0
-
ARGV[2] = 30000 (30秒)
-
ARGV[3] = "client1_thread1"
-
当前锁状态:
my_lock: {client1_thread1: 2}
(重入计数为2)
场景1:第一次解锁(重入情况)
-
验证持有权通过
-
计数减1 →
my_lock: {client1_thread1: 1}
-
更新过期时间
-
返回 0(部分解锁)
场景2:最后一次解锁
-
验证持有权通过
-
计数减1 →
my_lock: {client1_thread1: 0}
-
删除
my_lock
key -
发布解锁消息
-
返回 1(完全解锁)
场景3:非持有者尝试解锁
-
验证持有权失败(hexists 返回 0)
-
直接返回 nil(解锁失败)
这个脚本是 Redisson 分布式锁释放操作的核心,通过精细的状态判断和原子性操作,确保了锁释放的安全性和可靠性,同时支持可重入特性和等待者通知机制。