欢迎光临
我们一直在努力

分布式锁-redission可重入锁原理

一、前言:为什么 Redisson 的锁能“可重入”?

你是否好奇:

  • 同一个线程多次调用 lock.lock(),为什么不会死锁?
  • 业务执行了 1 小时,锁为什么没有自动过期?
  • 解锁时如何确保只删自己的锁?

这些问题的答案,都藏在 Redisson 可重入锁(Reentrant Lock) 的精巧设计中。

本文将带你深入源码级原理,拆解 Redisson 如何利用 Redis + Lua + Watchdog 实现一个安全、可重入、自动续期的分布式锁。


二、核心数据结构:Redis 中的锁长什么样?

当你执行:

RLock lock = redisson.getLock("myLock");
lock.lock();

Redisson 会在 Redis 中写入一条 Hash 结构 数据:

> HGETALL myLock
1) "b8a7f5e3-1234-5678-9abc-def012345678:1" # field
2) "1" # value(重入次数)

🔑 关键设计:

  • Key:用户指定的锁名(如 "myLock")
  • Field:UUID + ":" + threadId
    • UUID:标识当前 Redisson 客户端实例(防跨 JVM 误删)
    • threadId:标识当前线程(支持同 JVM 多线程隔离)
  • Value:重入次数(int 类型)

✅ 这个 Hash 结构是实现可重入性和安全性的核心!


三、加锁流程:Lua 脚本保证原子性

Redisson 加锁通过 Lua 脚本 原子执行,脚本逻辑如下(简化版):

— KEYS[1] = 锁名(myLock)
— ARGV[1] = 过期时间(毫秒)
— ARGV[2] = UUID:threadId

if (redis.call('exists', KEYS[1]) == 0) then
— 锁不存在 → 初始化
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil — 成功
elseif (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 — 成功
else
— 锁被其他线程持有 → 返回剩余过期时间
return redis.call('pttl', KEYS[1])
end

🧠 流程图解:

┌───────────────┐
│ 加锁请求 │
└───────┬───────┘

┌───────────────────────┐
│ Redis 执行 Lua 脚本 │
└───────────┬───────────┘

┌───────────────────────┐
│ 锁不存在? → 初始化 Hash│
│ 是当前线程?→ 重入+1 │
│ 其他线程? → 返回等待 │
└───────────────────────┘

✅ 优势:整个判断+设置过程原子执行,杜绝并发漏洞


四、自动续期:Watchdog 机制(防业务超时)

问题场景:

  • 锁默认 TTL = 30 秒
  • 业务执行耗时 50 秒
  • 30 秒时锁自动过期 → 其他线程获得锁 → 并发安全破坏!

Redisson 解法:Watchdog(看门狗)

工作原理:
  • 当调用 lock.lock()(无参)时,不设置 TTL
  • 同时启动后台线程(Watchdog)
  • 每隔 10 秒(internalLockLeaseTime / 3 = 30000 / 3)检查:
    • 如果当前线程仍持有该锁 → 执行 PEXPIRE myLock 30000
  • 直到调用 unlock(),Watchdog 停止续期
  • 💡 关键点:只要业务没结束,锁就永远不会过期!

    源码位置(Redisson 3.x):
    • org.redisson.RedissonLock#lock()
    • org.redisson.LockPubSub#scheduleExpirationRenewal()

    五、解锁流程:安全删除 + 通知等待线程

    解锁同样通过 Lua 脚本 原子执行:

    — KEYS[1] = 锁名
    — ARGV[1] = UUID:threadId
    — ARGV[2] = 通道名(用于 pub/sub)

    if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil — 不是当前线程的锁
    end

    local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)
    if (counter > 0) then
    — 重入次数 > 0,只减计数,不删锁
    redis.call('pexpire', KEYS[1], ARGV[3]) — 续期
    return 0
    else
    — 重入次数归零,删除锁
    redis.call('del', KEYS[1])
    — 发布解锁消息,唤醒等待线程
    redis.call('publish', ARGV[2], ARGV[1])
    return 1
    end

    📌 重点:

    • 只允许持有者解锁
    • 支持重入次数递减
    • 解锁后通过 Pub/Sub 通知其他等待线程

    六、可重入性演示

    RLock lock = redisson.getLock("myLock");

    lock.lock(); // 第1次:Hash 初始化 { "uuid:1": "1" }
    lock.lock(); // 第2次:重入 → { "uuid:1": "2" }
    // 业务逻辑…
    lock.unlock(); // 计数减1 → { "uuid:1": "1" }
    lock.unlock(); // 计数归零 → 删除 key

    ✅ 同一线程可无限次加锁,只需成对 unlock


    七、与其他方案对比

    特性手写 Redis 锁ZooKeeperRedisson 可重入锁
    可重入 ❌ 需自行实现 ✅ 支持 ✅ 原生支持
    自动续期 ❌ 需自己写线程 ✅ 临时节点 ✅ Watchdog
    性能 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
    实现复杂度 低(一行代码)

    八、注意事项与最佳实践

    1. 必须配对使用 lock/unlock

    // ✅ 正确
    lock.lock();
    try {
    // 业务
    } finally {
    lock.unlock(); // 必须在 finally 中!
    }

    2. 避免在异步线程中解锁

    • 子线程的 threadId 与加锁线程不同 → 无法解锁
    • 解决方案:传递 RLock 对象,或使用 CompletableFuture 回调

    3. 不要混用有参/无参 lock()

    • lock(10, SECONDS):不会启动 Watchdog,10 秒后强制过期
    • lock():启动 Watchdog,永不超时(直到 unlock)

    九、结语

    感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

    赞(0)
    未经允许不得转载:171主机测评 » 分布式锁-redission可重入锁原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址