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

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

你是否好奇:

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

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

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


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

当你执行:

java 复制代码
RLock lock = redisson.getLock("myLock");
lock.lock();

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

bash 复制代码
> 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 脚本 原子执行,脚本逻辑如下(简化版):

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(看门狗)

工作原理:
  1. 当调用 lock.lock()(无参)时,不设置 TTL
  2. 同时启动后台线程(Watchdog)
  3. 每隔 10 秒 (internalLockLeaseTime / 3 = 30000 / 3)检查:
    • 如果当前线程仍持有该锁 → 执行 PEXPIRE myLock 30000
  4. 直到调用 unlock(),Watchdog 停止续期

💡 关键点 :只要业务没结束,锁就永远不会过期!

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

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

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

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 通知其他等待线程

六、可重入性演示

java 复制代码
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 锁 ZooKeeper Redisson 可重入锁
可重入 ❌ 需自行实现 ✅ 支持 ✅ 原生支持
自动续期 ❌ 需自己写线程 ✅ 临时节点 ✅ Watchdog
性能 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
实现复杂度 高 中 低(一行代码)

八、注意事项与最佳实践

1. 必须配对使用 lock/unlock

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

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

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

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

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

九、结语

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

相关推荐
8Qi88 小时前
DDD架构演进
java·分布式·mvc·ddd
方方洛8 小时前
ray教程-23-附录C-术语表
人工智能·分布式·机器学习
海棠Flower未眠15 小时前
下单请求处理流程:商品维度分布式锁与 Redis 预扣库存
redis·分布式·junit
罗四强17 小时前
第10篇:打包上架:签名、打包 HAP、发布到华为应用市场
分布式·操作系统·harmonyos·arkts·鸿蒙系统
秋饼17 小时前
Redis 全面解析:从数据结构到主从/哨兵/集群、分布式锁与面试高频考点,代码到原理一次讲透
数据库·redis·分布式·面试·后端开发
二月夜17 小时前
Redis 高频面试题全解:缓存三大问题、大 key 治理与分布式锁之争(2026 招聘 JD 真题复盘)
redis·分布式·后端·缓存
樱花落木兰17 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程
真上帝的左手19 小时前
12. 消息队列-Kafka进阶
分布式·中间件·kafka
Thanks_ks2 天前
Cookie 和 Session 到底是什么关系——登录态是怎么维持的
分布式·http·jwt·cookie·session·后端开发·登录态
Sayai2 天前
Kafka 跨数据中心日志同步实战:MirrorMaker 2 选型对比 + Flink 中继任务落地(双中心互备与消费者组隔离避坑)
大数据·运维·分布式·flink·kafka·消息队列·高可用