一、前言:为什么你的分布式锁"有时失效"?
你是否遇到过这些问题:
- 业务执行了 40 秒,但锁 30 秒就没了,导致超卖?
- 多个服务同时抢锁,有的直接失败,没有重试?
- 明明加了锁,却出现并发修改?
根本原因,很可能在于你没用好 Redisson 的两大核心机制:
🔹 锁重试(Lock Retry) ------ 智能等待,避免立即失败
🔹 WatchDog(看门狗) ------ 自动续期,防止业务超时丢锁
本文将带你彻底掌握这两大机制的工作原理、使用方式与避坑指南。
二、机制一:锁重试(Lock Retry)------ 智能等待,不轻易放弃
场景问题:
当多个线程/服务同时请求同一把锁时,未获得锁的一方该如何处理?
- 直接报错? → 用户体验差
- 无限等待? → 线程阻塞,资源耗尽
Redisson 的解决方案:
提供 带超时的 tryLock() ,支持最大等待时间 + 持有时间。
API 示例:
java
RLock lock = redisson.getLock("inventory:lock");
// 最多等待 3 秒获取锁,获得后最多持有 10 秒
boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (isLocked) {
try {
// 扣减库存等业务逻辑
deductStock();
} finally {
lock.unlock();
}
} else {
throw new BusinessException("系统繁忙,请稍后再试");
}
🔍 底层原理:
- 客户端尝试加锁(执行 Lua 脚本)
- 若失败,订阅 Redis 的解锁频道(Pub/Sub)
- 启动定时任务,每隔 1/3 持有时间(如 10s/3 ≈ 3.3s)重试
- 一旦持有者释放锁,会发布消息,唤醒所有等待者竞争
✅ 优势:避免轮询浪费 CPU,实现高效等待
三、机制二:WatchDog(看门狗)------ 自动续期,永不丢锁
经典问题重现:
java
lock.lock(); // 默认 TTL = 30 秒
Thread.sleep(40_000); // 业务耗时 40 秒
// 此时锁已过期!其他线程可进入 → 并发安全破坏!
WatchDog 如何解决?
只要你不 unlock,锁就永远不会过期!
工作流程:
- 调用无参
lock()时,不设置 TTL - 同时启动后台线程(WatchDog)
- 每隔 10 秒 (
internalLockLeaseTime / 3 = 30000 / 3)检查:- 如果当前线程仍持有该锁 → 执行
PEXPIRE myLock 30000
- 如果当前线程仍持有该锁 → 执行
- 调用
unlock()后,取消 WatchDog 定时任务
📊 时间线示例:
t=0s → 加锁,启动 WatchDog
t=10s → WatchDog 续期(TTL 重置为 30s)
t=20s → 再次续期
t=25s → unlock() 被调用
t=25s+ → WatchDog 停止,锁被删除
💡 关键点 :WatchDog 只在 无参 lock() 时启用!
若使用
lock(10, SECONDS),不会启动 WatchDog,10 秒后强制过期!
四、源码级解析:WatchDog 如何实现?
核心类:org.redisson.RedissonLock
1. 加锁时注册续期任务:
java
void scheduleExpirationRenewal(...) {
// 将锁信息存入本地 Map
ExpirationEntry entry = new ExpirationEntry();
expirationRenewalMap.put(lockName, entry);
// 启动定时任务
Timeout task = commandExecutor.getConnectionManager().newTimeout(
timeout -> {
// 发送 PEXPIRE 命令续期
renewExpirationAsync(threadId);
// 递归调度下一次
scheduleExpirationRenewal(...);
},
internalLockLeaseTime / 3, // 默认 10 秒
TimeUnit.MILLISECONDS
);
entry.addTimeout(task);
}
2. 解锁时取消任务:
java
void cancelExpirationRenewal(...) {
ExpirationEntry entry = expirationRenewalMap.remove(lockName);
if (entry != null) {
for (Timeout timeout : entry.getTimeouts()) {
timeout.cancel(); // 取消所有定时任务
}
}
}
✅ 设计精妙:通过本地 Map + Netty Timeout 实现高效管理
五、锁重试 + WatchDog 联合使用示例
java
public void seckill(Long productId, Long userId) {
RLock lock = redisson.getLock("seckill:product:" + productId);
try {
// 最多等待 2 秒抢锁,抢到后由 WatchDog 自动续期
if (lock.tryLock(2, TimeUnit.SECONDS)) {
// 业务可能耗时较长(如调用风控、支付)
doSeckillBusiness(productId, userId);
} else {
log.warn("用户 {} 抢购商品 {} 失败:锁等待超时", userId, productId);
throw new BusinessException("活动太火爆,请稍后再试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 安全解锁
}
}
}
✅ 最佳实践:
- 抢锁阶段 :用
tryLock(waitTime)避免无限阻塞- 持有阶段:依赖 WatchDog 自动续期,无需担心业务超时
六、常见误区与避坑指南
❌ 误区 1:认为 lock(30, SECONDS) 会自动续期
真相 :只有无参
lock()才启动 WatchDog !
lock(30, SECONDS)表示"最多持有 30 秒",到期强制释放。
❌ 误区 2:在异步线程中 unlock()
java
lock.lock();
CompletableFuture.runAsync(() -> {
lock.unlock(); // ❌ threadId 不匹配,无法解锁!
});
正解 :解锁必须在加锁的同一线程中进行
❌ 误区 3:忘记判断是否持有锁就 unlock()
风险 :可能抛出异常或误删他人锁
建议 :使用lock.isHeldByCurrentThread()判断
七、性能与可靠性对比
| 方案 | 是否支持重试 | 是否自动续期 | 适用场景 |
|---|---|---|---|
| 手写 Redis SET NX | ❌ | ❌ | 简单低并发 |
Redisson lock() |
✅(阻塞) | ✅ | 通用高可靠 |
Redisson tryLock(w, h) |
✅(超时) | ✅(仅无参 holdTime) | 用户交互型业务 |
📌 推荐:
- 后台任务 → 用
lock()- Web 请求 → 用
tryLock(waitTime, leaseTime)
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!