分布式锁-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)

九、结语

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

相关推荐
珠海西格2 小时前
远动通信装置为何是电网安全运行的“神经中枢”?
大数据·服务器·网络·数据库·分布式·安全·区块链
CTO Plus技术服务中3 小时前
分布式存储HBase开发与运维教程
运维·分布式·hbase
飞乐鸟4 小时前
Github 16.8k Star!推荐一款开源的高性能分布式对象存储系统!
分布式·开源·github
panzer_maus5 小时前
分布式锁的概念
分布式
Lansonli5 小时前
大数据Spark(七十九):Action行动算子countByKey和countByValue使用案例
大数据·分布式·spark
少许极端7 小时前
Redis入门指南(八):从零到分布式缓存-集群机制、缓存机制、分布式锁
redis·分布式·缓存·分布式锁
珠海西格16 小时前
“主动预防” vs “事后补救”:分布式光伏防逆流技术的代际革命,西格电力给出标准答案
大数据·运维·服务器·分布式·云计算·能源
小邓吖19 小时前
自己做了一个工具网站
前端·分布式·后端·中间件·架构·golang
曹天骄1 天前
基于 Cloudflare Worker 构建分布式测速调度系统:KV 与 D1 数据层设计实战教程
分布式·缓存