Redisson 实现的分布式锁核心原理是利用 Redis 的原子操作、数据结构和发布订阅机制,在单节点或集群环境下提供互斥、可重入、自动续期(看门狗)、公平锁等特性。其核心机制如下:
核心原理与流程
-
锁获取 (加锁)
-
Lua 脚本保证原子性: 当线程尝试获取锁时,Redisson 会执行一个 Lua 脚本到 Redis 服务器。脚本的核心逻辑是:
luaif (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (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; end; return redis.call('pttl', KEYS[1]);
-
参数解释:
KEYS[1]
: 锁的名字(Key),例如myLock
。ARGV[1]
: 锁的初始生存时间(TTL),单位毫秒(默认 30 秒)。ARGV[2]
: 客户端唯一标识(getLockName(threadId)
) 。通常由UUID:threadId
组成(如8743c9c0-0795-4907-87fd-6c719a6b4586:1
),用于标识哪个客户端(JVM)的哪个线程持有锁。
-
脚本逻辑详解:
- 锁不存在 (
exists == 0
): 创建一个 Hash 结构,Key 是锁名,Hash 的 Field 是客户端唯一标识ARGV[2]
,Value 设置为 1(表示加锁次数为 1)。设置这个 Key 的过期时间为ARGV[1]
(默认 30 秒)。返回nil
表示加锁成功。 - 锁已存在,且持有者是当前线程 (
hexists == 1
): 将 Hash 中该客户端标识对应的 Value(加锁次数)加 1。重新设置 Key 的过期时间为ARGV[1]
。返回nil
表示重入加锁成功。 - 锁已存在,且持有者不是当前线程: 返回该 Key 剩余的生存时间(
pttl
),单位毫秒。
- 锁不存在 (
-
-
锁获取失败与重试
- 如果脚本返回的不是
nil
(即返回了一个数字,代表剩余 TTL),说明锁被其他线程持有。 - 客户端(Redisson 内部)会通过 Redis 的发布订阅(Pub/Sub) 机制,订阅一个与该锁名称相关的特定频道(通常是
redisson_lock__channel:{lockName}
)。 - 客户端会等待 ,直到收到锁释放的通知消息,或者等待时间超过
ARGV[1]
返回的剩余 TTL(取较小值)。 - 在等待超时或被唤醒后,客户端会再次尝试执行步骤 1 的 Lua 脚本获取锁(循环重试)。
- 可配置性: 重试次数、重试间隔、等待时间上限可以通过
lock()
方法的参数或配置进行调整。
- 如果脚本返回的不是
-
锁续期(看门狗 - Watchdog)
- 目的: 防止业务逻辑执行时间超过锁的初始 TTL(默认 30 秒)导致锁被 Redis 自动删除,而业务逻辑仍在运行,其他线程可能获得锁引发数据不一致。
- 机制:
- 只有在没有指定锁超时时间 (
leaseTime
)调用lock()
时,看门狗才会启动。 - 当加锁成功(脚本返回
nil
)后,Redisson 会在持有锁的客户端启动一个后台定时任务(守护线程)。 - 这个任务默认每 10 秒检查一次客户端是否还持有该锁(通过检查 Hash 结构中对应的 Field 是否存在且 Value > 0)。
- 如果客户端仍然持有锁,它会重置 该锁 Key 的过期时间回初始值(默认 30 秒)。即
pexpire KEYS[1] 30000
。
- 只有在没有指定锁超时时间 (
- 效果: 只要持有锁的客户端(JVM)还在正常运行且没有主动释放锁,看门狗会不断续期,保证业务逻辑有足够时间执行。
-
锁释放 (解锁)
-
Lua 脚本保证原子性: 释放锁时同样执行一个 Lua 脚本:
luaif (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;
-
参数解释:
KEYS[1]
: 锁名。KEYS[2]
: 发布订阅的频道名(redisson_lock__channel:{lockName}
)。ARGV[1]
: 发布的消息内容(通常是解锁消息)。ARGV[2]
: 锁的 TTL(用于重入锁释放后重置 TTL)。ARGV[3]
: 客户端唯一标识。
-
脚本逻辑详解:
- 检查持有者 (
hexists == 0
): 如果锁不存在或当前线程不是持有者(Field 不存在),直接返回nil
(可能锁已过期或已被释放,或者不是持有者尝试释放)。这是防止误删他人锁的关键。 - 减少重入计数 (
hincrby ... -1
): 将当前线程对应的重入计数器减 1。 - 计数器仍大于 0 (
counter > 0
): 说明是重入锁,当前线程尚未完全释放锁。重置锁的过期时间为ARGV[2]
。返回0
。 - 计数器等于 0 (
counter == 0
): 说明这是最后一次释放(重入计数归零)。- 删除整个锁 Key (
del KEYS[1]
)。 - 发布消息 (
publish KEYS[2], ARGV[1]
): 向该锁的订阅频道发送一条解锁消息,通知所有正在等待这个锁的客户端(触发步骤 2 中的重试)。返回1
。
- 删除整个锁 Key (
- 检查持有者 (
-
关键特性支撑
- 互斥性: Lua 脚本的原子执行确保只有一个客户端能在锁不存在时成功创建锁(Hash 结构)。
- 可重入性: 使用 Hash 结构的 Field(客户端标识)和 Value(重入次数)记录同一个线程多次加锁。加锁时计数加 1,解锁时计数减 1,计数归零才真正删除锁 Key。
- 避免死锁/自动释放:
- 基础: 依赖 Redis Key 的 TTL,即使持有锁的客户端崩溃,锁也会在超时后自动删除。
- 增强 (看门狗): 后台线程自动续期,防止业务执行时间超过初始 TTL 导致的锁提前失效。
- 容错性 (单点基本可用): 基于单 Redis 节点即可工作(主从异步复制下存在极端情况下的安全性问题,生产环境建议用 RedLock 或 WAIT 命令增强,但 RedLock 也有争议)。
- 高效等待: 利用 Redis 的发布订阅机制,避免客户端无意义的轮询(忙等待),减少网络和 Redis 压力。
- 防止误删: 解锁脚本严格检查客户端标识,确保只有锁的持有者才能释放锁(删除 Key)。
总结
Redisson 分布式锁的核心在于通过 Lua 脚本在 Redis 上原子性地操作一个 Hash 结构来实现锁状态的管理(创建、重入计数、释放) ,同时利用 TTL 防止死锁 ,利用看门狗机制续期 防止业务未完成锁超时失效,并利用发布订阅机制实现高效的锁等待通知。这种设计提供了高性能、高可靠且功能丰富(可重入、公平锁等)的分布式锁实现。