> 技术文档 > Redisson解锁脚本分析

Redisson解锁脚本分析

rotected RFuture 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 分布式锁释放操作的核心,通过精细的状态判断和原子性操作,确保了锁释放的安全性和可靠性,同时支持可重入特性和等待者通知机制。