Redisson加锁脚本分析

发布于:2025-07-24 ⋅ 阅读:(19) ⋅ 点赞:(0)
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;
  1. redis.call('exists', KEYS[1]) == 0

    • 检查锁(KEYS[1])是否不存在

    • exists 命令返回 0 表示 key 不存在

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

    • 如果锁不存在,使用 Hash 结构初始化锁

    • hincrby 命令在 Hash 中为字段 ARGV[2](客户端/线程标识)设置值 1

    • 这表示当前客户端/线程第一次获取该锁

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

    • 为锁设置过期时间(毫秒)

    • 防止客户端崩溃导致锁永远无法释放

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

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

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

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

    • 如果锁已由当前线程持有,增加重入计数

    • 实现可重入锁的关键

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

    • 更新锁的过期时间

    • 保证业务执行期间锁不会过期

  4. return nil

    • 返回 nil 表示获取锁成功(重入成功)

第三部分:锁被其他线程持有

lua

return redis.call('pttl', KEYS[1]);
  1. 如果前两个条件都不满足(锁存在但不是当前线程持有)

  2. 返回锁的剩余生存时间(毫秒)

    • pttl 命令返回 key 的剩余生存时间

    • 客户端可以根据这个时间决定是否继续等待

脚本执行流程总结

  1. 锁不存在 → 初始化锁(设置持有者为当前线程,计数=1)→ 设置过期时间 → 获取成功

  2. 锁已存在且由当前线程持有 → 增加重入计数 → 更新过期时间 → 获取成功

  3. 锁被其他线程持有 → 返回锁的剩余生存时间 → 获取失败

关键设计点

  1. 原子性操作:整个逻辑在一个 Lua 脚本中完成,保证原子性

  2. 可重入支持:通过 Hash 结构的字段值记录重入次数

  3. 锁续期:每次获取锁(包括重入)都会更新过期时间

  4. 竞争处理:返回锁的剩余时间让客户端决定是否等待

  5. 资源标识:ARGV[2] 唯一标识客户端/线程,避免不同客户端或线程间的误释放

实际应用场景示例

假设:

  • KEYS[1] = "my_lock"

  • ARGV[1] = 30000 (30秒过期时间)

  • ARGV[2] = "client1_thread1"

场景1:第一次获取锁

  1. 锁不存在 → 初始化 Hash: my_lock: {client1_thread1: 1}

  2. 设置过期时间 30秒

  3. 获取成功

场景2:同一线程重入

  1. 锁已存在且由 client1_thread1 持有

  2. 增加计数: my_lock: {client1_thread1: 2}

  3. 更新过期时间

  4. 获取成功

场景3:其他线程尝试获取

  1. 锁已存在且由 client1_thread1 持有

  2. 返回锁的剩余生存时间(如 25000 毫秒)

  3. 获取失败

这个脚本是 Redisson 分布式锁实现的核心,通过 Redis 的单线程特性和 Lua 脚本的原子性,实现了高效可靠的分布式锁机制。


网站公告

今日签到

点亮在社区的每一天
去签到