Redisson 四大核心机制实现原理详解
一、可重入锁(Reentrant Lock)
可重入锁是什么?
通俗定义
可重入锁类似于一把“智能锁”,它能识别当前的锁持有者是否是当前线程:
- 如果是,则允许线程重复获取锁(重入),并记录重入次数。
- 如果不是,则其他线程必须等待锁释放后才能获取。
典型场景
当一个线程调用了一个被锁保护的方法A,而方法A内部又调用了另一个被同一锁保护的方法B时,如果锁不可重入,线程会在调用方法B时被自己阻塞(死锁)。可重入锁允许这种嵌套调用。
public class Demo {private final Lock lock = new SomeLock(); // 假设这是一个锁public void methodA() {lock.lock();try {methodB(); // 调用另一个需要加锁的方法} finally {lock.unlock();}}public void methodB() {lock.lock();try {// 业务逻辑} finally {lock.unlock();}} }
- 如果锁不可重入 线程进入
methodA
获取锁后,调用methodB
时再次尝试加锁,会因为锁已被自己持有而永久阻塞(死锁)。- 如果锁可重入 线程在
methodB
中能成功获取锁,计数器从1
增加到2
,释放时计数器递减,最终正常释放。
实现原理:通过 Redis 的 Hash 结构实现线程级锁的可重入性。
-
数据结构:
- Key:锁名称(如
lock:order:1001
)。 - Field:客户端唯一标识(
UUID + 线程ID
),如b983c153-7091-42d8-823a-cb332d52d2a6:1
。 - Value:锁的 重入次数(初始为 1,重入时递增)。
- Key:锁名称(如
-
加锁逻辑:
- 首次加锁:执行 Lua 脚本,若 Key 不存在,创建 Hash 并设置重入次数为 1。
-- KEYS[1]=锁名, ARGV[1]=锁超时时间, ARGV[2]=线程唯一ID if (redis.call('exists', KEYS[1]) == 0) then -- 如果锁不存在redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 创建Hash,记录线程重入次数redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置锁超时时间return nil; -- 返回成功 end;
- 重入加锁:若 Field 匹配当前线程,重入次数 +1。
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;
- 首次加锁:执行 Lua 脚本,若 Key 不存在,创建 Hash 并设置重入次数为 1。
-
释放锁:减少重入次数,归零时删除 Hash。
-- KEYS[1]: 锁名称(如 my_lock) -- KEYS[2]: 发布订阅的频道名 -- ARGV[1]: 解锁消息标识(如 0) -- ARGV[2]: 锁的过期时间(毫秒) -- ARGV[3]: 客户端唯一标识(UUID + 线程ID)-- 检查锁是否存在且属于当前线程 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) thenreturn 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; -- 返回0表示未完全释放 else-- 计数器归零,删除锁并发布释放通知redis.call('del', KEYS[1]);redis.call('publish', KEYS[2], ARGV[1]);return 1; -- 返回1表示锁已完全释放 end;
二、锁重试机制(Retry Mechanism)
重试机制的触发条件
当调用
tryLock(long waitTime, long leaseTime, TimeUnit unit)
方法时,若waitTime > 0
,Redisson 会启用重试机制。例如:java// 10秒内不断重试获取锁,获取成功后持有锁60秒 lock.tryLock(10, 60, TimeUnit.SECONDS);
若首次获取锁失败,进入重试流程。
实现原理: 事件驱动优先,主动轮询兜底
-
首次尝试获取锁
- 原子性操作:通过 Lua 脚本尝试获取锁(检查锁是否存在或是否属于当前线程)。
- 失败返回值:若锁被其他线程持有,返回锁的剩余存活时间(
ttl
)。
-
订阅锁释放事件
- 创建监听频道:订阅 Redis 频道
redisson_lock__channel:{lockName}
。 - 事件驱动优化:避免频繁轮询,仅当锁释放时触发重试,减少无效请求。
// 伪代码:订阅锁释放事件 RFuture<RedissonLockEntry> future = subscribe(lockName); RedissonLockEntry entry = get(future);
- 创建监听频道:订阅 Redis 频道
-
循环重试(主动轮询 + 事件触发)
- 计算剩余等待时间:基于
waitTime
和已消耗时间,动态调整剩余等待窗口。 - 双重检测逻辑:
- 主动轮询:定期(默认间隔 100ms ~ 300ms)执行 Lua 脚本尝试获取锁。
- 事件触发:收到锁释放通知后立即尝试获取锁。
- 退避策略:每次重试失败后,采用随机递增的等待时间(避免多个客户端同时竞争导致雪崩)。
关键代码逻辑(简化):
- 计算剩余等待时间:基于
long remainingTime = waitTime; // 剩余等待时间
long startTime = System.currentTimeMillis();while (remainingTime > 0) {// 1. 尝试获取锁Long ttl = tryAcquire(leaseTime, unit); // 调用Lua脚本if (ttl == null) {return true; // 获取成功}// 2. 计算剩余时间long elapsed = System.currentTimeMillis() - startTime;remainingTime -= elapsed;if (remainingTime <= 0) {break; // 超时退出}// 3. 等待锁释放事件或超时entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); // 基于信号量等待// 4. 更新剩余时间remainingTime -= (System.currentTimeMillis() - startTime - elapsed);
}
return false; // 超时未获取
- 超时终止
- 时间窗口耗尽:若总耗时超过
waitTime
,终止重试并返回失败。 - 资源清理:取消 Redis 订阅,释放连接。
- 时间窗口耗尽:若总耗时超过
三、WatchDog 看门狗(锁续期机制)
防止业务执行时间超过锁的过期时间,导致锁提前释放。
启用看门狗需满足以下条件之一:
- 未显式指定锁的租约时间(leaseTime): 例如调用
lock.tryLock()
或lock.lock()
时不传leaseTime
参数。- 显式设置租约时间为
-1
: 例如lock.tryLock(10, -1, TimeUnit.SECONDS)
。注意:若指定了固定的
leaseTime
(如lock.tryLock(10, 30, TimeUnit.SECONDS)
),看门狗不会启动,锁会在 30 秒后自动释放。
实现原理:后台线程自动续期锁,防止业务未完成时锁过期。
-
触发条件:未指定锁超时时间(如
lock.lock()
)。 -
续期逻辑:
-
定时任务:默认每 10 秒(
lockWatchdogTimeout / 3
)续期一次。 -
续期命令:重置锁的过期时间为 30 秒(默认值)。
-- KEYS[1]: 锁名称 -- ARGV[1]: 过期时间(默认30秒) -- ARGV[2]: 客户端唯一标识 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('pexpire', KEYS[1], ARGV[1]);return 1; end; return 0;
-
-
终止条件:
- 锁被释放(
unlock()
调用)。 - 客户端断开连接或线程中断。
- 锁被释放(
四、主从一致性(MultiLock/RedLock)
Redis 主从复制是异步的,若主节点宕机且锁未同步到从节点,可能导致多个客户端同时持有锁。
实现原理:基于多数派原则,向多个独立节点加锁。
-
MultiLock 流程:
- 加锁:向所有节点发送加锁请求,需 半数以上成功(如 3 节点至少 2 个成功)。
- 容错:允许最多
⌊(N-1)/2⌋
个节点故障(如 5 节点允许 2 个故障)。 - 解锁:无论加锁是否成功,向所有节点发送解锁命令。
-
RedLock 算法增强:
- 时钟同步:要求节点使用 NTP 同步时间,锁有效期需包含时钟漂移。
- 加锁验证:计算加锁耗时,确保有效时间未耗尽。
-
配置示例:
RLock lock1 = redissonClient1.getLock("lock"); RLock lock2 = redissonClient2.getLock("lock"); RLock multiLock = new RedissonMultiLock(lock1, lock2); multiLock.lock(); try {// 业务逻辑 } finally {multiLock.unlock(); }
五、总结
机制 | 实现原理 |
---|---|
可重入锁 | 使用 Redis Hash 结构存储锁名、线程唯一标识(UUID+线程ID)和重入次数。同一线程多次获取锁时重入次数递增,释放时递减,归零后删除锁。 |
锁重试 | 通过 Pub/Sub 订阅锁释放事件 避免轮询;失败后按退避策略(默认 1.5 秒)重试,直到超时或成功。 |
WatchDog | 后台线程每 10 秒(默认)检查锁持有状态,若锁存在则续期(重置过期时间至 30 秒)。未指定锁超时时间时自动启用。 |
主从一致性 | 使用 MultiLock/RedLock:向多个独立节点加锁,需半数以上成功;解锁时向所有节点发送命令,解决主从异步复制导致的锁失效。 |