Redisson解锁脚本分析

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;
  1. redis.call('hexists', KEYS[1], ARGV[3]) == 0

    • 检查锁是否由当前线程持有

    • hexists 命令检查 Hash 中是否存在指定字段

    • 返回 0 表示当前线程不持有该锁

  2. return nil

    • 如果锁不是由当前线程持有,直接返回 nil

    • 防止误删其他线程持有的锁

第二部分:减少重入计数

lua

复制代码
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
  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;
  1. counter > 0

    • 检查减少后的计数是否仍大于 0

    • 表示当前线程仍然持有该锁(重入情况)

  2. redis.call('pexpire', KEYS[1], ARGV[2])

    • 更新锁的过期时间

    • 保证锁在剩余持有期间不会过期

  3. return 0

    • 返回 0 表示锁未完全释放(只是减少了重入计数)

第四部分:完全释放锁

lua

复制代码
else 
    redis.call('del', KEYS[1]); 
    redis.call('publish', KEYS[2], ARGV[1]); 
    return 1; 
end;
  1. redis.call('del', KEYS[1])

    • 当计数减到 0 时,完全释放锁

    • 删除 Redis 中的锁键

  2. redis.call('publish', KEYS[2], ARGV[1])

    • 向锁的频道发布解锁消息

    • 通知其他等待获取锁的客户端

    • ARGV[1] 是固定消息内容(0L)

  3. return 1

    • 返回 1 表示锁已完全释放

第五部分:默认返回

lua

复制代码
return nil;
  • 理论上不会执行到这里

  • 作为脚本的默认返回值

脚本执行流程总结

  1. 检查锁持有权 → 如果不是当前线程持有 → 返回 nil(解锁失败)

  2. 是当前线程持有 → 减少重入计数 →

    • 如果计数仍>0 → 更新过期时间 → 返回 0(部分解锁)

    • 如果计数=0 → 删除锁 → 发布解锁消息 → 返回 1(完全解锁)

关键设计点

  1. 线程安全验证:先验证锁的持有者,防止误删

  2. 可重入支持:通过计数机制实现

  3. 锁续期:部分解锁时更新过期时间

  4. 通知机制:完全释放时发布消息通知等待者

  5. 原子性操作:整个解锁过程在一个 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. 验证持有权通过

  2. 计数减1 → my_lock: {client1_thread1: 1}

  3. 更新过期时间

  4. 返回 0(部分解锁)

场景2:最后一次解锁

  1. 验证持有权通过

  2. 计数减1 → my_lock: {client1_thread1: 0}

  3. 删除 my_lock key

  4. 发布解锁消息

  5. 返回 1(完全解锁)

场景3:非持有者尝试解锁

  1. 验证持有权失败(hexists 返回 0)

  2. 直接返回 nil(解锁失败)

这个脚本是 Redisson 分布式锁释放操作的核心,通过精细的状态判断和原子性操作,确保了锁释放的安全性和可靠性,同时支持可重入特性和等待者通知机制。

相关推荐
C182981825751 天前
Redisson加锁脚本分析
redisson
phantomsee9 天前
Redis学习系列之——高并发应用的缓存问题(二)
redis·redisson
马里奥Marioぅ17 天前
Redis主从切换踩坑记:当Redisson遇上分布式锁的“死亡连接“
redis·分布式锁·redisson·故障转移
xujinwei_gingko21 天前
接口幂等性
分布式锁·redisson·接口幂等性
沉着的码农1 个月前
【分布式】Redisson滑动窗口限流器原理
java·redis·分布式·redisson
jstart千语1 个月前
【Redisson】锁可重入原理
redis·分布式·redisson
啾啾Fun2 个月前
【Java微服务组件】分布式协调P4-一文打通Redisson:从API实战到分布式锁核心源码剖析
java·redis·分布式·微服务·lua·redisson
小马爱打代码2 个月前
Redisson - 实现延迟队列
redisson
快乐肚皮2 个月前
Redisson学习专栏(四):实战应用(分布式会话管理,延迟队列)
分布式·学习·redisson·延迟队列·分布式会话