分布式锁-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"
  • FieldUUID + ":" + 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)

九、结语

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

相关推荐
1104.北光c°4 小时前
我理解的Leaf号段模式:美团分布式ID生成系统
java·开发语言·笔记·分布式·github·leaf
天涯明月19936 小时前
服务网格完全指南:从基础概念到生产实践
java·服务器·数据库·分布式·微服务
筱顾大牛7 小时前
Redission快速入门---分布式锁
java·redis·分布式·缓存
江不清丶8 小时前
Kafka重平衡(Rebalance)深度解析:原理、影响与优化策略
分布式·kafka
Coder_Boy_1 天前
分布式系统核心技术完整梳理(含分库分表、分布式事务、熔断补偿)
jvm·分布式·spring·中间件
摇滚侠1 天前
Java 项目教程《黑马商城-MQ 篇》,分布式架构项目,从开发到部署
java·分布式·架构
蜜獾云1 天前
Kafka(4)-kafka生产环境规划部署
分布式·kafka
若水不如远方1 天前
分布式一致性协议(五):殊途同归 —— ZAB 协议与 ZooKeeper 架构
分布式·后端·zookeeper
星辰_mya1 天前
分布式锁:跨 JVM 的“工商局备案章”
jvm·分布式·面试
wanhengidc1 天前
服务器分布式存储的功能
运维·服务器·分布式