Redisson加锁脚本分析

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;
  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 脚本的原子性,实现了高效可靠的分布式锁机制。

相关推荐
会编程的林俊杰9 天前
Redisson中的分布式锁
redis·分布式·redisson
鼠鼠我捏,要死了捏22 天前
基于Redisson实现高并发分布式锁性能优化实践指南
性能优化·分布式锁·redisson
C1829818257525 天前
Redisson解锁脚本分析
redisson
phantomsee1 个月前
Redis学习系列之——高并发应用的缓存问题(二)
redis·redisson
马里奥Marioぅ1 个月前
Redis主从切换踩坑记:当Redisson遇上分布式锁的“死亡连接“
redis·分布式锁·redisson·故障转移
xujinwei_gingko1 个月前
接口幂等性
分布式锁·redisson·接口幂等性
沉着的码农2 个月前
【分布式】Redisson滑动窗口限流器原理
java·redis·分布式·redisson
jstart千语2 个月前
【Redisson】锁可重入原理
redis·分布式·redisson
啾啾Fun2 个月前
【Java微服务组件】分布式协调P4-一文打通Redisson:从API实战到分布式锁核心源码剖析
java·redis·分布式·微服务·lua·redisson