分布式锁的可重入是怎么实现的?
简化版
可重入指同一个客户端/线程可以多次获取同一把锁而不会把自己锁死(比如方法 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 脚本逻辑大致是:
- 锁不存在(
EXISTS myLock为 0)→HSET myLock uuid:threadId 1,设过期时间,成功。 - 锁存在且
HEXISTS myLock uuid:threadId(是自己持有)→HINCRBY myLock uuid:threadId 1(计数 +1),刷新过期时间,成功。 - 锁存在但持有者是别人 → 返回锁的剩余过期时间,加锁失败(客户端据此等待重试)。
解锁的 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、解锁计数归零才删锁。