Redisson分布式锁可重入实现

wen java案例 3

本文目录导读:

Redisson分布式锁可重入实现

  1. 数据结构设计
  2. 加锁流程(Lua 脚本实现)
  3. 解锁流程(Lua 脚本实现)
  4. 与 Watch Dog 机制的配合
  5. 与传统 Redis 锁的关键区别

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,再判断是否 DELpexpire
超时续期 需要自行实现(如单独线程)。 Watch Dog 内置守护线程自动续期。

Redisson 分布式锁的可重入实现依赖以下几个关键设计:

  1. 底层数据结构:使用 Hash 而非 String,通过 UUID:threadId 作为 field 来标识锁的唯一持有者。
  2. 重入计数:使用 hincrby 命令对 Hash 中的 value(重入次数)进行增加或减少。
  3. 原子操作:所有加锁、解锁及重入逻辑都封装在 Lua 脚本 中,保证了整个流程的原子性。
  4. TTL 联动:每次成功重入都会刷新 Hash 的 TTL,确保持有锁的线程不会被意外释放。

抱歉,评论功能暂时关闭!