java
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]);",
Redisson 加锁 Lua 脚本详细分析
下面我将逐行详细解析 Redisson 加锁的核心 Lua 脚本,这个脚本实现了分布式锁的获取逻辑,包括锁的初始化、可重入支持和锁竞争处理。
脚本完整代码
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]);
参数说明
-
KEYS[1]: 锁的名称(Redis key)
-
ARGV[1]: 锁的过期时间(毫秒)
-
ARGV[2]: 锁的唯一标识(通常由客户端ID + 线程ID组成)
逐行解析
第一部分:锁不存在时的初始化
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;
-
redis.call('exists', KEYS[1]) == 0
-
检查锁(KEYS[1])是否不存在
-
exists
命令返回 0 表示 key 不存在
-
-
redis.call('hincrby', KEYS[1], ARGV[2], 1)
-
如果锁不存在,使用 Hash 结构初始化锁
-
hincrby
命令在 Hash 中为字段 ARGV[2](客户端/线程标识)设置值 1 -
这表示当前客户端/线程第一次获取该锁
-
-
redis.call('pexpire', KEYS[1], ARGV[1])
-
为锁设置过期时间(毫秒)
-
防止客户端崩溃导致锁永远无法释放
-
-
return nil
- 返回 nil 表示获取锁成功
第二部分:锁已存在且由当前线程持有
lua
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;
-
redis.call('hexists', KEYS[1], ARGV[2]) == 1
-
检查锁是否已由当前线程持有
-
hexists
检查 Hash 中是否存在指定字段
-
-
redis.call('hincrby', KEYS[1], ARGV[2], 1)
-
如果锁已由当前线程持有,增加重入计数
-
实现可重入锁的关键
-
-
redis.call('pexpire', KEYS[1], ARGV[1])
-
更新锁的过期时间
-
保证业务执行期间锁不会过期
-
-
return nil
- 返回 nil 表示获取锁成功(重入成功)
第三部分:锁被其他线程持有
lua
return redis.call('pttl', KEYS[1]);
-
如果前两个条件都不满足(锁存在但不是当前线程持有)
-
返回锁的剩余生存时间(毫秒)
-
pttl
命令返回 key 的剩余生存时间 -
客户端可以根据这个时间决定是否继续等待
-
脚本执行流程总结
-
锁不存在 → 初始化锁(设置持有者为当前线程,计数=1)→ 设置过期时间 → 获取成功
-
锁已存在且由当前线程持有 → 增加重入计数 → 更新过期时间 → 获取成功
-
锁被其他线程持有 → 返回锁的剩余生存时间 → 获取失败
关键设计点
-
原子性操作:整个逻辑在一个 Lua 脚本中完成,保证原子性
-
可重入支持:通过 Hash 结构的字段值记录重入次数
-
锁续期:每次获取锁(包括重入)都会更新过期时间
-
竞争处理:返回锁的剩余时间让客户端决定是否等待
-
资源标识:ARGV[2] 唯一标识客户端/线程,避免不同客户端或线程间的误释放
实际应用场景示例
假设:
-
KEYS[1] = "my_lock"
-
ARGV[1] = 30000 (30秒过期时间)
-
ARGV[2] = "client1_thread1"
场景1:第一次获取锁
-
锁不存在 → 初始化 Hash:
my_lock: {client1_thread1: 1}
-
设置过期时间 30秒
-
获取成功
场景2:同一线程重入
-
锁已存在且由 client1_thread1 持有
-
增加计数:
my_lock: {client1_thread1: 2}
-
更新过期时间
-
获取成功
场景3:其他线程尝试获取
-
锁已存在且由 client1_thread1 持有
-
返回锁的剩余生存时间(如 25000 毫秒)
-
获取失败
这个脚本是 Redisson 分布式锁实现的核心,通过 Redis 的单线程特性和 Lua 脚本的原子性,实现了高效可靠的分布式锁机制。