本文目录导读:

Redisson 的分布式锁可重入(Reentrant)实现主要基于 Redis 的 Hash 数据结构 以及 Lua 脚本的原子性。
核心原理是:在加锁时,不再使用简单的 SET key value NX PX,而是使用一个 Hash 来存储锁的持有者信息和重入次数。
数据结构设计
Redisson 将锁的 Key 对应一个 Redis Hash,这个 Hash 包含两个关键的 Field:
UUID:threadId: 这是锁的持有者标识。UUID是 Redisson 客户端的唯一 ID(每个客户端实例不同),threadId是当前线程的 ID,这个组合确保了不同客户端、不同线程的唯一性。value: 一个整数(AtomicInteger),代表当前线程的重入次数。
假设客户端 A 的 UUID 是 abc123,线程 1 的 ID 是 456,锁的 Key 是 myLock,那么在 Redis 中,这个锁的存储结构是:
# key: myLock # field: abc123:456 # value: 1
加锁流程(Lua 脚本实现)
Redisson 使用 Lua 脚本来保证加锁和重入逻辑的原子性,核心代码如下(简化后的逻辑):
-- KEYS[1] 是锁的 key,"myLock"
-- ARGV[1] 是锁的自动释放时间(TTL),单位毫秒
-- ARGV[2] 是锁的持有者标识,格式为 "UUID:threadId","abc123:456"
-- 1. 检查锁是否已存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 锁不存在,对该线程来说就是第一次加锁
-- 使用 Hash 存储,field 是线程标识,value 是 1(重入次数)
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置 Hash 的过期时间(TTL)
redis.call('pexpire', KEYS[1], ARGV[1]);
-- 返回 nil(或者0),表示加锁成功
return nil;
end;
-- 2. 锁已存在,检查当前线程是否已经是锁的持有者
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 是同一个线程在尝试再次加锁(可重入)
-- 将重入次数 +1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重新设置过期时间(刷新 TTL)
redis.call('pexpire', KEYS[1], ARGV[1]);
-- 返回 nil(或者0),表示加锁成功
return nil;
end;
-- 3. 锁已被其他线程持有,加锁失败
-- 返回锁的剩余存活时间(TTL),单位毫秒
return redis.call('pttl', KEYS[1]);
对应的 Java 方法 tryLockInnerAsync 中的核心逻辑与上述 Lua 脚本一致。
解锁流程(Lua 脚本实现)
解锁时,同样使用 Lua 脚本来保证原子性,核心操作是对重入次数进行减 1。
-- KEYS[1] 是锁的 key,"myLock"
-- KEYS[2] 是通知消息的 channel 名称(用于发布订阅模式通知等待线程)
-- ARGV[1] 是锁的自动释放时间(TTL)
-- ARGV[2] 是锁的持有者标识,格式为 "UUID:threadId"
-- 1. 检查当前线程是否持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
-- 当前线程不持有锁(可能是锁已过期或被其他线程抢占)
return nil;
end;
-- 2. 将重入次数减 1
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
-- 3. 判断减 1 后的结果
if (counter > 0) then
-- 重入次数还大于 0,说明有多次重入,锁还没有完全释放
-- 刷新锁的过期时间(重置 TTL)
redis.call('pexpire', KEYS[1], ARGV[1]);
-- 返回 0,表示解锁成功,但锁依然存在(只是重入次数减了)
return 0;
else
-- 重入次数变成了 0,说明锁需要被完全释放
-- 删除这个 Hash
redis.call('del', KEYS[1]);
-- 发布一条消息,通知正在等待这个锁的其他线程(例如在锁等待队列中的线程)
redis.call('publish', KEYS[2], ARGV[2]);
-- 返回 1,表示锁已完全释放
return 1;
end;
与 Watch Dog 机制的配合
Redisson 的 Watch Dog(看门狗) 机制与可重入锁的实现紧密相关:
- 当一个线程成功加锁(包括重入加锁)后,Watch Dog 会启动一个后台定时任务。
- 每次重入时: Lua 脚本中会执行
pexpire命令,刷新整个 Hash 的 TTL,Watch Dog 也会周期性地(默认每 10 秒)检查锁的 TTL,并对其进行续期(重置为 30 秒)。 - 每次解锁后: 如果重入次数减 1 后仍然大于 0,Lua 脚本会
pexpire刷新 TTL,Watch Dog 继续工作,如果重入次数变为 0(锁完全释放),则删除 Key,Watch Dog 也会随之停止。
与传统 Redis 锁的关键区别
| 特性 | 传统 SET key value NX PX |
Redisson 可重入锁 |
|---|---|---|
| 数据结构 | 简单的 String | Hash |
| 可重入性 | 不支持,同一线程再次加锁会返回失败。 | 支持,通过 Hash 的 field 和 hincrby 实现。 |
| 原子性保障 | 单命令原子。 | Lua 脚本 保证 exists / hexists / hincrby / pexpire 等操作的原子性。 |
| 释放逻辑 | 直接 DEL。 |
先 hincrby ... -1,再判断是否 DEL 或 pexpire。 |
| 超时续期 | 需要自行实现(如单独线程)。 | Watch Dog 内置守护线程自动续期。 |
Redisson 分布式锁的可重入实现依赖以下几个关键设计:
- 底层数据结构:使用 Hash 而非 String,通过
UUID:threadId作为 field 来标识锁的唯一持有者。 - 重入计数:使用
hincrby命令对 Hash 中的 value(重入次数)进行增加或减少。 - 原子操作:所有加锁、解锁及重入逻辑都封装在 Lua 脚本 中,保证了整个流程的原子性。
- TTL 联动:每次成功重入都会刷新 Hash 的 TTL,确保持有锁的线程不会被意外释放。