Redisson的看门狗相关问题
首先要明确一点,看门狗机制的使用方式是:在加锁的时候不加任何参数,也就是:
RLock lock = redisson.getLock("myLock");
try {lock.lock(); // 阻塞式加锁// 业务逻辑...
} finally {lock.unlock(); // 确保在finally中释放锁
} //这样子加锁会使用看门狗,也就是说这个锁会一直的续命下去,只要代码没有执行到 lock.unlock() 这一行
如果加了参数,那么和看门狗这个机制就完全不搭边,时间到了,强制放锁
通过 lock.lock(long leaseTime, TimeUnit unit) 指定锁的最大持有时间,超时后锁也会强制释放:
RLock lock = redisson.getLock("myLock");
try {// 设置锁超时时间为10秒,看门狗不会续期lock.lock(10, TimeUnit.SECONDS); // 业务逻辑...
} finally {if (lock.isLocked() && lock.isHeldByCurrentThread()) {lock.unlock();}
}
那么说到这里就有一个很明显的问题,不加参数的时候,如果try里面有了死锁,或者一不小心写了 Thread.sleep(Integer.Max)这样的代码,那么这个锁就会一直不放,这显然是不对的,如果加了参数,强制放锁的时候,逻辑又没有执行完,就会有数据不一致,或者说并发的问题。怎么办?
问题核心
- 不加超时时间(依赖看门狗):业务死锁时,锁会无限续期,导致其他线程永远无法获取锁(死锁风险)。
- 加超时时间(强制释放):若业务未执行完但锁已超时释放,其他线程可能并发操作(数据不一致风险)。
解决方案
1. 合理设置超时时间(平衡点)
-
原则:
- 超时时间(
leaseTime
)应 略大于 业务平均执行时间(如业务通常耗时 5秒,设置leaseTime=10秒
)。 - 通过压测或监控统计业务耗时,动态调整超时时间。
- 超时时间(
-
示例:
RLock lock = redisson.getLock("orderLock"); try {// 设置超时时间为平均耗时的2倍lock.lock(10, TimeUnit.SECONDS); // 业务逻辑(假设通常耗时3~5秒)... } finally {lock.unlock(); }
2. 异步续期 + 主动心跳检测
-
适用场景:业务耗时波动大(如依赖外部服务响应)。
-
实现方式:
- 仍设置较短的
leaseTime
(如 10秒)。 - 在业务代码中定期向 Redis 发送“心跳”(如每 5秒更新一次锁的时间戳)。
- 若业务卡死,心跳停止,锁超时释放。
- 仍设置较短的
-
伪代码:
RLock lock = redisson.getLock("orderLock"); try {lock.lock(10, TimeUnit.SECONDS);while (业务未完成) {// 业务逻辑...redisson.getBucket("lock:heartbeat:" + lockName).set(System.currentTimeMillis());Thread.sleep(5000); // 每5秒发送心跳} } finally {lock.unlock(); }
3. 分段锁(减小锁粒度)
- 原理:将大锁拆分为多个小锁,降低单个锁的持有时间。
- 示例:
// 对订单ID分段加锁(如按订单ID哈希取模) int segment = orderId.hashCode() % 16; RLock lock = redisson.getLock("orderLock:" + segment); lock.lock(10, TimeUnit.SECONDS);
4. 兜底补偿机制
- 场景:锁超时释放后,其他线程并发操作导致数据不一致。
- 方案:
- 记录操作日志或版本号,通过定时任务检查并修复不一致数据。
- 使用数据库乐观锁(如
UPDATE table SET value=newVal WHERE id=xxx AND version=oldVersion
)。
5. 结合 tryLock 和熔断机制
-
策略:
- 尝试获取锁(带短时间等待)。
- 若获取失败,触发熔断(如返回“系统繁忙”)。
- 避免线程堆积导致雪崩。
-
代码示例:
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) { // 最多等待1秒,锁超时10秒try {// 业务逻辑...} finally {lock.unlock();} } else {throw new BusyException("系统繁忙,请重试"); }
最佳实践总结
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
合理设置超时时间 | 业务耗时稳定 | 简单可靠 | 需准确预估耗时 |
异步续期 + 心跳 | 业务耗时波动大 | 灵活控制锁生命周期 | 实现复杂 |
分段锁 | 高并发场景 | 减少锁竞争 | 需设计合理分段规则 |
兜底补偿 | 对一致性要求不苛刻 | 最终一致 | 修复延迟 |
tryLock + 熔断 | 高并发且允许短暂失败 | 避免系统过载 | 用户体验可能受影响 |
终极建议
- 优先使用
lock(leaseTime)
:根据业务 P99 耗时设置超时时间(如 P99=3秒,设leaseTime=5秒
)。 - 关键业务加监控:对锁持有时间超过阈值(如 80% leaseTime)触发告警。
- 设计幂等操作:即使锁超时导致并发,业务逻辑也能安全重试(如唯一键防重)。
注:没有完美的方案,需根据业务容忍度权衡 “死锁风险” 和 “并发风险”。
Redisson中的锁是什么结构,存的是什么?底层实现是 setnx 么?
Redisson 分布式锁的底层实现
Redisson 的分布式锁(如 RLock
)并非直接使用 Redis 的 SETNX
命令,而是基于 Lua 脚本 和 Hash 结构 实现了一套更复杂的机制,支持可重入、锁续期(看门狗)、公平锁等高级特性。
1. 锁的存储结构
Redisson 的锁在 Redis 中存储为一个 Hash 结构,Key 的名称即锁的名称(如 myLock
),Hash 的字段和值如下:
Hash Field | Value | 说明 |
---|---|---|
UUID:threadId | 计数器(重入次数) | 锁的持有者标识(UUID 为客户端唯一ID,threadId 为线程ID),计数器表示重入次数。 |
mode | redisson_lock | 标识这是一个 Redisson 锁。 |
leaseTime | 过期时间(毫秒) | 锁的租约时间(看门狗续期时会更新)。 |
示例 Redis 数据:
HGETALL myLock
1) "b983c153-7421-469a-addf-8de0345aaedd:1" # UUID:线程ID
2) "1" # 重入次数
3) "mode"
4) "redisson_lock"
5) "leaseTime"
6) "30000" # 30秒过期时间
2. 加锁流程(核心 Lua 脚本)
Redisson 加锁的核心是通过 Lua 脚本 保证原子性,逻辑如下:
(1) 加锁脚本(lock.lua
)
-- 参数:KEYS[1]=锁名称, ARGV[1]=锁过期时间, ARGV[2]=客户端UUID:线程ID
-- 1. 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then-- 锁不存在,直接加锁redis.call('hset', KEYS[1], ARGV[2], 1); -- 设置 Hash 字段(UUID:threadId: 1)redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间return nil; -- 返回 nil 表示加锁成功
end;-- 2. 锁已存在,检查是否当前线程持有
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;-- 3. 锁被其他线程持有,返回剩余存活时间(TTL)
return redis.call('pttl', KEYS[1]);
关键点:
- 原子性:通过 Lua 脚本保证
exists
+hset
+pexpire
的原子操作。 - 可重入:同一个线程多次加锁时,
hincrby
增加计数器。 - 锁竞争:如果锁被其他线程持有,返回剩余 TTL,客户端会循环尝试。
(2) 看门狗续期机制
如果使用 lock()
不加超时时间,Redisson 会启动一个 看门狗线程(Watchdog),默认每 10秒 检查锁是否仍被当前线程持有,如果是,则续期 30秒:
-- 续期脚本
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('pexpire', KEYS[1], ARGV[1]); -- 重置过期时间return 1;
end;
return 0;
3. 解锁流程
解锁时,Redisson 会先检查锁是否仍被当前线程持有,然后减少重入计数或直接删除锁:
-- 参数:KEYS[1]=锁名称, ARGV[1]=过期时间, ARGV[2]=客户端UUID:线程ID
-- 1. 检查锁是否属于当前线程
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) thenreturn nil;
end;-- 2. 减少重入计数
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then-- 仍持有锁(重入情况),续期redis.call('pexpire', KEYS[1], ARGV[1]);return 0;
else-- 完全释放锁,删除 Keyredis.call('del', KEYS[1]);-- 发布解锁消息(通知其他等待线程)redis.call('publish', KEYS[2], ARGV[3]);return 1;
end;
4. 与 SETNX
的区别
对比项 | Redisson 锁 | SETNX 锁 |
---|---|---|
数据结构 | Hash(存储客户端ID、重入次数) | String(简单 Key-Value) |
可重入 | ✅ 支持(通过计数器) | ❌ 不支持 |
自动续期 | ✅ 看门狗机制 | ❌ 需手动实现 |
公平锁 | ✅ 支持(通过 Redis 队列) | ❌ 不支持 |
原子性 | ✅ Lua 脚本保证 | ⚠️ 需配合 EXPIRE (非原子) |
5. 总结
- Redisson 锁的底层是 Hash 结构,存储
客户端ID:线程ID
和 重入次数。 - 加锁/解锁通过 Lua 脚本保证原子性,而非简单的
SETNX
。 - 看门狗机制 自动续期,避免锁过期。
- 支持可重入、公平锁等高级特性,比
SETNX
更强大。
适用场景:
- 需要可重入锁 → Redisson
- 简单互斥锁 →
SETNX + EXPIRE
(但需自行处理原子性问题)
看门狗是如何知道某个线程是否存活,或者说某个线程是否还在执行?
Redisson 看门狗(Watchdog)如何检测线程存活?
Redisson 的看门狗机制并不直接监控 JVM 线程是否存活,而是通过以下方式间接判断业务是否仍在执行,从而决定是否续期锁:
1. 看门狗的核心逻辑
(1) 锁续期的触发条件
- 当使用
lock()
不加超时时间时,Redisson 会启动一个后台线程(看门狗),默认每隔 10秒 执行一次续期检查。 - 续期条件:
- 锁仍存在于 Redis 中(未被手动释放或过期)。
- 锁的持有者仍是当前客户端和线程(通过 Redis Hash 中的
UUID:threadId
字段验证)。
(2) 续期流程
- 检查锁归属:
看门狗通过 Lua 脚本检查 Redis 中的锁是否仍由当前客户端(UUID
)和线程(threadId
)持有:if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 检查UUID:threadId是否存在redis.call('pexpire', KEYS[1], ARGV[1]); -- 续期锁return 1; end; return 0;
- 续期或终止:
- 如果锁仍属于当前线程,则重置过期时间为 30秒(默认)。
- 如果锁已被释放或归属变更,看门狗停止续期。
2. 如何判断业务线程是否仍在执行?
看门狗不直接监控业务线程状态,而是通过以下间接机制推断:
(1) JVM 线程存活间接关联
-
锁释放的触发条件:
- 业务线程正常执行完成后,调用
lock.unlock()
,删除 Redis 中的锁。 - 如果业务线程崩溃或阻塞,
unlock()
不会被调用,锁会因看门狗续期而长期存在。
- 业务线程正常执行完成后,调用
-
看门狗续期的隐含假设:
- 只要 JVM 进程存活,且持有锁的线程未调用
unlock()
,看门狗就会认为业务仍在执行(即使线程实际已阻塞或死锁)。
- 只要 JVM 进程存活,且持有锁的线程未调用
(2) 异常情况处理
-
线程崩溃或 JVM 退出:
- 看门狗线程是后台守护线程(Daemon Thread),如果 JVM 进程退出,看门狗会停止运行,锁最终会在 Redis 中自然过期(默认 30秒后释放)。
- 无需依赖线程存活检测,依赖 Redis 的过期机制兜底。
-
网络分区或 Redis 不可用:
- 看门狗无法连接 Redis 时,续期失败,锁会在过期后自动释放。
3. 为什么不用 Java 线程监控?
- 分布式环境限制:
- Redisson 是分布式锁,可能跨多个 JVM 或机器,无法直接监控其他 JVM 的线程状态。
- 复杂度与可靠性:
- 线程监控需依赖 JVM 的
Thread.isAlive()
,但分布式场景下不可靠(如网络分区时误判)。
- 线程监控需依赖 JVM 的
- Redis 是唯一信源:
- 所有锁状态由 Redis 存储,看门狗只需关心 Redis 中的数据,无需跨进程协调。
4. 关键结论
问题 | Redisson 的解决方案 |
---|---|
如何知道线程是否存活? | 不直接检测,而是通过锁是否被主动释放(unlock() )判断。未释放则续期。 |
线程崩溃后锁何时释放? | 依赖 Redis 的 Key 过期机制(默认 30秒),看门狗停止续期后锁自动释放。 |
看门狗如何保证不续死锁? | 业务必须显式调用 unlock() ,否则需设置超时时间(lock(10, TimeUnit.SECONDS) )避免无限续期。 |
5. 最佳实践
- 避免依赖看门狗无限续期:
- 优先使用
lock(leaseTime, unit)
设置合理的超时时间,即使业务卡死,锁也会自动释放。
lock.lock(10, TimeUnit.SECONDS); // 明确指定超时时间
- 优先使用
- 确保锁被释放:
- 将
unlock()
放在finally
块中,防止异常导致锁泄漏。
try {lock.lock();// 业务逻辑... } finally {lock.unlock(); }
- 将
- 监控长耗时锁:
- 对持有时间超过阈值的锁触发告警(如通过 Redisson 的
RLock.getRemainingLeaseTime()
)。
- 对持有时间超过阈值的锁触发告警(如通过 Redisson 的
总结
Redisson 的看门狗通过 定期续期 Redis 锁 和 检查锁归属 间接判断业务是否执行中,而非直接监控线程状态。
设计核心思想:分布式锁的安全性应完全由 Redis 保证,避免依赖 JVM 层面的不可靠检测。
也就是说看门狗其实和 jvm中线程完全没关系,只不过是运行借助jvm运行的一个守护线程,这个守护线程只和redis通信,不和jvm中其他线程做任何的数据交互
核心总结
-
看门狗的本质:
-
看门狗线程的启动时机,全局看门狗线程池:Redisson 在客户端初始化时(即 RedissonClient 创建时)会启动一个名为redisson-timeout 的守护线程池。这个线程池并非专为看门狗设计,而是用于处理所有需要超时控制的异步任务(包括但不限于锁的续期)。
-
看门狗线程的懒加载:只有在首次使用无超时锁(lock())时,才会从该线程池中分配一个线程作为专属看门狗,用于定期续期该锁。如果只用带超时的锁(lock(leaseTime, unit)),则不会分配看门狗线程。
-
仅与 Redis 交互,通过定期执行 Lua 脚本检查/续期锁,完全不感知 JVM 内其他线程的状态(如是否阻塞、崩溃)。
-
-
与 JVM 线程的关系:
- 不依赖线程监控:看门狗不会检查业务线程(如
Thread.isAlive()
),也不与业务线程直接通信。 - 仅依赖 Redis 数据:通过 Redis 中锁的
UUID:threadId
字段是否存在,间接判断是否续期。
- 不依赖线程监控:看门狗不会检查业务线程(如
-
设计优势:
- 解耦:分布式锁的安全性完全由 Redis 保证,与 JVM 线程生命周期无关。
- 轻量:避免复杂的线程监控,仅通过 Redis 的键存在性检查实现高效续期。
- 可靠:即使业务线程死锁,锁最终会因看门狗停止(JVM 退出)或 Redis 过期而释放。
关键流程验证
场景 | 看门狗行为 | 锁的最终状态 |
---|---|---|
业务线程正常执行并解锁 | 看门狗检测到 unlock() 删除 Redis 中的 UUID:threadId ,停止续期。 | 立即释放 |
业务线程死锁/阻塞 | 看门狗持续续期(因 UUID:threadId 仍在 Redis 中),直到 JVM 退出或手动干预。 | 可能长期占用(需设置超时时间避免) |
JVM 崩溃 | 看门狗线程终止,无人续期,Redis 中的锁自然过期(默认30秒)。 | 自动释放 |
代码层面验证
1. 看门狗线程的启动
Redisson 客户端初始化时,会创建一个全局的 Watchdog
守护线程(单例):
public class RedissonClient {private final Watchdog watchdog = new Watchdog(); // 守护线程
}
2. 续期任务提交
业务线程加锁时,向看门狗注册续期任务(仅记录锁名称和 UUID:threadId
):
lock.lock(); // 无参加锁
// 内部逻辑:
watchdog.scheduleRenewal("myLock", "uuid:threadId");
3. 看门狗的工作内容
看门狗线程循环检查任务队列,通过 Redis 的 hexists
命令验证锁归属:
// 伪代码
while (!Thread.interrupted()) {for (LockTask task : tasks) {String result = redis.eval("if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +" redis.call('pexpire', KEYS[1], ARGV[2]); " +" return 1; " +"end; " +"return 0;",Collections.singletonList(task.getLockName()),task.getUUIDThreadId(), "30000" // 续期30秒);if (result.equals("0")) {removeTask(task); // 锁已失效,移出任务队列}}Thread.sleep(10000); // 默认10秒检查一次
}
常见问题解答
Q1: 如果业务线程卡死,看门狗会一直续期吗?
✅ 是的,因为看门狗只检查 Redis 中的 UUID:threadId
是否存在,不感知线程状态。需通过 lock(leaseTime, unit)
设置超时时间避免无限续期。
Q2: 为什么看门狗不直接监控业务线程?
- 分布式限制:无法跨 JVM 监控线程(如微服务场景)。
- 复杂度:线程监控需处理 JVM 崩溃、网络分区等边界条件,不可靠。
Q3: 看门狗会拖累 Redis 性能吗?
- 低开销:续期是低频操作(默认10秒一次),且 Lua 脚本轻量。
- 单线程优化:所有锁的续期由同一个看门狗线程批量处理。
最佳实践建议
- 永远设置超时时间:
lock.lock(10, TimeUnit.SECONDS); // 避免死锁导致无限续期
- 确保解锁:
try {lock.lock();// 业务逻辑... } finally {lock.unlock(); // 必须释放! }
- 监控长耗时锁:
long remainTime = lock.remainTimeToLive(); if (remainTime > THRESHOLD) {log.warn("锁持有时间过长:{}", lock.getName()); }
总结
✅ 看门狗是一个纯粹的 Redis 通信守护线程,通过定期检查 Redis 中的锁归属决定是否续期,与 JVM 线程完全解耦。
这种设计实现了分布式锁的高可靠性和低侵入性,但需开发者合理设置超时时间避免死锁。
看门狗是多久检查一次上锁的hash?续命多久?
在 Redisson 中,看门狗(Watchdog) 的检查与续期机制是锁续期的核心逻辑,具体行为如下:
1. 看门狗的检查与续期频率
- 默认检查间隔:每 10 秒(
lockWatchdogTimeout / 3
)检查一次锁状态。lockWatchdogTimeout
默认值为 30 秒(可通过Config.lockWatchdogTimeout
调整)。- 计算公式:
续期间隔 = lockWatchdogTimeout / 3
→30s / 3 = 10s
。
- 续期逻辑:每次检查时,如果锁仍被当前线程持有,则将锁的过期时间重置为
lockWatchdogTimeout
(默认 30 秒)。
2. 关键源码解析
(1)续期任务调度(scheduleExpirationRenewal
)
// RedissonLock.java
protected void scheduleExpirationRenewal(long threadId) {// 每 (lockWatchdogTimeout / 3) 秒执行一次续期Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) {// 执行续期逻辑renewExpiration();}}, lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS); // 默认 10 秒
}
(2)续期操作(renewExpiration
)
// RedissonLock.java
void renewExpiration() {// 通过 Lua 脚本重置锁的 TTLRFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 重置 TTL 为 lockWatchdogTimeout"return 1; " +"end; " +"return 0;",Collections.singletonList(getName()),internalLockLeaseTime, // ARGV[1]: lockWatchdogTimeout(默认 30000 毫秒)getLockName(threadId) // ARGV[2]: 客户端标识);// 递归调用,实现周期性续期future.onComplete((res, e) -> {if (e != null) {return;}if (res) {scheduleExpirationRenewal(threadId); // 继续下一次续期}});
}
3. 续期流程总结
- 首次加锁(无超时):
- 锁在 Redis 中的 TTL 默认设为
lockWatchdogTimeout
(30 秒)。 - 看门狗在
10 秒
后首次检查,若锁仍存在,则重置 TTL 为30 秒
。
- 锁在 Redis 中的 TTL 默认设为
- 周期性续期:
- 每
10 秒
检查一次,若锁未被释放,则再次续期30 秒
。 - 直到显式调用
unlock()
或客户端崩溃(此时续期任务停止,Redis 自动过期)。
- 每
4. 关键配置参数
参数 | 默认值 | 说明 |
---|---|---|
lockWatchdogTimeout | 30000 ms | 锁的默认存活时间(无超时锁的初始 TTL 和续期时长)。可通过 Config 自定义。 |
实际续期间隔 | 10000 ms | lockWatchdogTimeout / 3 ,即默认 10 秒。 |
5. 注意事项
- 仅对无超时锁生效:
显式指定leaseTime
的锁(如lock(10, SECONDS)
)不会触发看门狗续期。 - 避免长时间阻塞:
如果业务逻辑执行时间超过lockWatchdogTimeout
,且未及时续期,锁可能因 Redis TTL 到期而失效。 - 调整参数需谨慎:
- 增大
lockWatchdogTimeout
:降低续期频率,但锁释放延迟可能增加。 - 减小
lockWatchdogTimeout
:提高续期频率,但增加 Redis 和网络负载。
- 增大
6. 示例场景
// 无超时锁(启用看门狗)
RLock lock = redisson.getLock("myLock");
lock.lock(); // 看门狗每 10 秒续期一次,每次续 30 秒
try {// 执行业务逻辑(可能耗时较长)
} finally {lock.unlock(); // 主动释放,停止续期
}
通过这种机制,Redisson 确保了无超时锁的长期持有安全性,同时避免了因客户端崩溃导致的死锁问题。