← 返回题目列表

分布式锁的可重入是怎么实现的?

高频 中等 第 2 / 26 题 更新于 2026/07/28
可重入锁RedissonHash

简化版

可重入指同一个客户端/线程可以多次获取同一把锁而不会把自己锁死(比如方法 A 持锁后调用同样需要这把锁的方法 B)。实现思路和本地可重入锁(ReentrantLock)一样:记录「锁的持有者」和「重入次数」。加锁时如果发现持有者就是自己,就把重入计数 +1 而不是阻塞;解锁时计数 -1,减到 0 才真正释放锁。Redisson 用 Redis 的 Hash 结构{持有者标识: 重入次数} 来实现。

详细版

为什么需要可重入:如果锁不可重入,同一线程第二次获取同一把锁时会因为「锁已被占用」而阻塞——但占用者正是它自己,于是自己把自己锁死(死锁)。方法嵌套调用、递归、AOP 切面叠加等场景都需要可重入。

实现要点:锁里要存两个信息——

  • 持有者标识:谁持有这把锁(客户端 ID + 线程 ID)。
  • 重入次数:持有者重复获取了几次。

加锁逻辑

  • 锁不存在 → 创建锁,持有者设为自己,计数 = 1。
  • 锁存在且持有者是自己 → 计数 +1,加锁成功(可重入)。
  • 锁存在且持有者是别人 → 加锁失败,阻塞/重试。

解锁逻辑

  • 持有者是自己 → 计数 -1;计数 > 0 说明还有外层没退出,锁不删;计数 = 0 才真正删除锁。

Redisson 实现:用 Redis Hash,key 是锁名,field 是「客户端唯一标识(UUID:threadId)」,value 是重入次数,全程用 Lua 脚本保证原子。

完整版教学

一、不可重入会怎样(死锁自己)

看这段伪代码:

void methodA() {
  lock.lock();      // 第一次获取锁,成功
  methodB();
  lock.unlock();
}
void methodB() {
  lock.lock();      // 又要获取同一把锁
  // ...
  lock.unlock();
}

如果锁不可重入,methodB 里的 lock.lock() 会发现锁已被占用(其实是被 methodA 也就是自己占的),于是一直阻塞等待自己释放——但 methodA 要等 methodB 返回才会 unlock,形成自死锁。可重入锁能识别「这次来加锁的就是当前持有者」,直接放行并计数,避免死锁。

二、可重入的核心:持有者标识 + 重入计数

无论本地锁还是分布式锁,可重入的实现套路一致:锁不仅记录「被谁持有」,还记录「被持有者重入了几次」

  • 加锁:是新持有者就初始化计数为 1;是同一持有者就计数 +1。
  • 解锁:计数 -1,只有减到 0 才真正释放锁(对应最外层那次加锁的退出)。

计数保证了「加锁和解锁次数配对」——加锁 3 次要解锁 3 次才真正放锁,防止内层方法一 unlock 就把外层还需要的锁释放了。

记忆点:可重入 = 记住「持有者是谁」+「重入了几次」,同一持有者加锁只加计数、解锁只减计数,减到 0 才真正释放。

三、Redisson 用 Hash 实现

Redisson 的可重入锁用 Redis 的 Hash 结构

  • key:锁名,如 myLock
  • field:客户端线程唯一标识,如 uuid:threadId
  • value:重入次数。

加锁的 Lua 脚本逻辑大致是:

  1. 锁不存在(EXISTS myLock 为 0)→ HSET myLock uuid:threadId 1,设过期时间,成功。
  2. 锁存在且 HEXISTS myLock uuid:threadId(是自己持有)→ HINCRBY myLock uuid:threadId 1(计数 +1),刷新过期时间,成功。
  3. 锁存在但持有者是别人 → 返回锁的剩余过期时间,加锁失败(客户端据此等待重试)。

解锁的 Lua 脚本:HINCRBY myLock uuid:threadId -1,若结果 > 0 只刷新过期不删锁;若结果 = 0 则 DEL myLock 真正释放并发布释放消息通知等待者。全程 Lua 保证原子。

四、和 ReentrantLock 的类比

Java 的 ReentrantLock 也是这套:AQS 的 state 字段记录重入次数,exclusiveOwnerThread 记录持有线程。同一线程重入时 state++,释放时 state--,减到 0 才真正释放并唤醒后继。分布式可重入锁只是把「线程 + 计数」这套状态从 JVM 内存搬到了 Redis Hash / ZooKeeper 节点上,原理完全一致——理解了本地可重入锁,分布式可重入锁就是「换个地方存状态」。

五、可重入锁落地自检

可重入锁要同时记录“谁持有”和“重入几次”。比如服务 A 的 request-1 第一次进入库存扣减时 count=1,内部又调用同一资源保护方法时 count=2,第一次 unlock 只能减到 1,第二次 unlock 才能真正释放。少了计数会提前释放,少了 owner 会让别的请求误释放。

六、常见误区与追问

这道题不能只背概念,要把「分布式锁可重入」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。

回答层次要讲清的内容容易漏掉的边界
核心结论可重入表示同一持有者重复加同一把锁不会死锁,通常用 owner 标识和计数实现不要停在名词解释
流程机制判断锁不存在则创建 owner 和 count=1 -> 同 owner 重入时 count+1 -> 不同 owner 获取失败 -> unlock 时 count-1 -> count=0 才删除锁说明触发方、参与方、状态变化和兜底
工程取舍线程 A 第一次加锁 count=1,内部方法再次加锁 count=2,释放两次后才真正删除锁分布式锁只能控制临界区进入,不能替代业务幂等和状态校验
分布式锁可重入 面试拆解:
1. 判断锁不存在则创建 owner 和 count=1
2. 同 owner 重入时 count+1
3. 不同 owner 获取失败
4. unlock 时 count-1
5. count=0 才删除锁

记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「分布式锁可重入」这道题,不要把相邻概念混成一段泛泛的分布式套话。

  • 误区:分布式锁天然可重入。 Redis 基础 SETNX 锁不可重入,需要额外记录 owner 和计数。
  • 误区:只用线程 ID 就能标识 owner。 分布式环境还要包含客户端、进程或请求维度,避免不同 JVM 线程 ID 冲突。
  • 误区:释放一次就能删除锁。 重入锁必须按计数释放,计数归零才真正释放。
  • 追问:Redisson 如何实现可重入? 用 Redis Hash 记录 owner 字段和重入次数,并配合 Lua 原子操作。
  • 追问:可重入解决什么问题? 同一业务调用链内重复进入同一临界区时避免自我死锁。
  • 追问:可重入会带来什么风险? 忘记成对释放会导致计数不归零,只能等 TTL 或续期停止。

七、加强记忆

可重入 = 同一持有者可多次获取同一把锁而不自锁死。实现核心是记录「持有者标识 + 重入次数」:加锁时若持有者是自己就计数 +1(放行),解锁时计数 -1、减到 0 才真正释放。Redisson 用 Redis Hash(field=uuid:threadId,value=重入次数)+ Lua 原子实现,原理和 Java ReentrantLock 的 AQS state 计数完全一致,只是状态从 JVM 内存搬到了 Redis。注意用 Hash 而非 String、解锁计数归零才删锁。