一、前言:为什么你的分布式锁“有时失效”?

你是否遇到过这些问题:

  • 业务执行了 40 秒,但锁 30 秒就没了,导致超卖?
  • 多个服务同时抢锁,有的直接失败,没有重试?
  • 明明加了锁,却出现并发修改?

根本原因,很可能在于你没用好 Redisson 的两大核心机制

🔹 锁重试(Lock Retry) —— 智能等待,避免立即失败
🔹 WatchDog(看门狗) —— 自动续期,防止业务超时丢锁

本文将带你彻底掌握这两大机制的工作原理、使用方式与避坑指南


二、机制一:锁重试(Lock Retry)—— 智能等待,不轻易放弃

场景问题:

当多个线程/服务同时请求同一把锁时,未获得锁的一方该如何处理

  • 直接报错? → 用户体验差
  • 无限等待? → 线程阻塞,资源耗尽

Redisson 的解决方案:

提供 带超时的 tryLock(),支持最大等待时间 + 持有时间

API 示例:
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("系统繁忙,请稍后再试");
}

🔍 底层原理:

  1. 客户端尝试加锁(执行 Lua 脚本)
  2. 若失败,订阅 Redis 的解锁频道(Pub/Sub)
  3. 启动定时任务,每隔 1/3 持有时间(如 10s/3 ≈ 3.3s)重试
  4. 一旦持有者释放锁,会发布消息,唤醒所有等待者竞争

✅ 优势:避免轮询浪费 CPU,实现高效等待


三、机制二:WatchDog(看门狗)—— 自动续期,永不丢锁

经典问题重现:

lock.lock(); // 默认 TTL = 30 秒
Thread.sleep(40_000); // 业务耗时 40 秒
// 此时锁已过期!其他线程可进入 → 并发安全破坏!

WatchDog 如何解决?

只要你不 unlock,锁就永远不会过期!

工作流程:
  1. 调用无参 lock() 时,不设置 TTL
  2. 同时启动后台线程(WatchDog)
  3. 每隔 10 秒internalLockLeaseTime / 3 = 30000 / 3)检查:
    • 如果当前线程仍持有该锁 → 执行 PEXPIRE myLock 30000
  4. 调用 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. 加锁时注册续期任务:
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. 解锁时取消任务:
void cancelExpirationRenewal(...) {
    ExpirationEntry entry = expirationRenewalMap.remove(lockName);
    if (entry != null) {
        for (Timeout timeout : entry.getTimeouts()) {
            timeout.cancel(); // 取消所有定时任务
        }
    }
}

✅ 设计精妙:通过本地 Map + Netty Timeout 实现高效管理


五、锁重试 + WatchDog 联合使用示例

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()

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)

八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐