续期的无限套娃

续期的无限套娃

scheduleExpirationRenewal最终会调用renewExpiration

private void renewExpiration() { // 这里的 1/3 是硬编码的规则 // 默认 lockWatchdogTimeout 是 30000ms // 所以每 10000ms 执行一次 Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { // 执行 Lua 脚本,把 ttl 重新刷回 30秒 RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (res) { // 如果续期成功,这就形成了递归调用:自己调自己 renewExpiration(); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); }

核心逻辑总结

  1. 三分之一原则:每隔锁超时时间的 1/3(默认10秒),检查一次。
  2. 无限递归:只要检查到锁还在,就重置过期时间,并注册下一次检查。
  3. 生死绑定:这个任务跑在客户端进程里,如果客户端宕机,任务停止,Redis 里的锁在 30秒 后自动过期。

四、我在生产环境踩过的坑:避坑实战

API 谁都会调,但能避开坑的才是老司机。这六个坑,都是真金白银换来的教训。

💣 陷阱一:好心办坏事 —— 弄死看门狗

这是新手最容易犯的错。

❌ 错误姿势

// 我怕死锁,所以强行指定 10秒 过期 lock.lock(10, TimeUnit.SECONDS); // 或者 lock.tryLock(1, 10, TimeUnit.SECONDS);

⚠️ 后果
Redisson 的看门狗(WatchDog)机制只有在你未指定锁过期时间时才会生效!
一旦你手动传了leaseTime,Redisson 就会认为你有自己的想法,不再插手。如果你的业务因为数据库卡顿跑了 15秒,第 10秒 时锁就会强制过期,其他线程长驱直入,爆发并发事故。

✅ 正确姿势
除非你非常确定业务能在指定时间内跑完,否则尽量不要传 leaseTime,让看门狗帮你自动续期

💣 陷阱二:锁粒度太粗 —— 全服暂停键

❌ 错误姿势

// 所有订单共用一把锁 RLock lock = redisson.getLock("LOCK_ORDER");

⚠️ 后果
这相当于把高速公路封成了独木桥。不管有多少个用户下单,同一时间只能处理一个。性能直接归零。

✅ 正确姿势
锁的粒度越细越好。只锁那个具体产生竞争的资源 ID。

// 只锁这个订单 RLock lock = redisson.getLock("order:pay:" + orderId);

💣 陷阱三:解锁的艺术 —— 谁加的锁谁来解

❌ 错误姿势

try { // 业务逻辑 } finally { lock.unlock(); // 直接解锁 }

⚠️ 后果

  1. 如果业务执行超时,锁已经被自动释放了,你再去unlock会抛出IllegalMonitorStateException
  2. 如果不小心解了别人的锁(虽然 Redisson 有 ID 校验防止误删,但异常处理依然重要)。

✅ 正确姿势

if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); }

💣 陷阱四:重入锁的"递归噩梦"

Redisson 的锁虽然是可重入的(Reentrant),但如果你在递归或嵌套调用中不注意,很容易逻辑混乱。

❌ 风险代码

void methodA() { lock.lock(); try { methodB(); // methodB 里又 lock 了一次 } finally { lock.unlock(); // 只解了一层 } }

⚠️ 后果
Redis 里的锁计数器(Counter)如果不归零,锁是不会释放的。确保你的加锁次数和解锁次数严格匹配

💣 陷阱五:主从切换的"幽灵锁"

这是 Redis 架构天生的短板。

  1. Client A 在Master节点拿到了锁。
  2. Master 还没来得及把锁同步给 Slave,就宕机了。
  3. Slave 升级为新的 Master。
  4. Client B 来加锁,发现新 Master 上没锁,于是也加锁成功

⚠️ 后果
A 和 B 同时持有了锁。
解法:如果你不能容忍这个概率(极低),请看下文的 RedLock,或者转投 Zookeeper。对于 99% 的业务,我们选择接受这个风险。


五、RedLock 的爱恨情仇

有些面试官特别喜欢问 RedLock,但在实际工作中,它是一个让人爱恨交加的存在。

1. 它是为了解决什么?

解决 Redis 主从集群在 Failover(故障转移)时可能丢锁的问题。

2. 怎么用?

你需要准备3个或5个完全独立的 Redis 实例(不是 Cluster,不是 Sentinel,就是干干净净的单实例)。

RLock lock1 = redissonInstance1.getLock("lock"); RLock lock2 = redissonInstance2.getLock("lock"); RLock lock3 = redissonInstance3.getLock("lock"); // 创建红锁 RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); try { // 同时向 3个 Redis 申请锁 // 只要有 > 1.5个 (即2个) 申请成功,就算赢 lock.lock(); // 业务逻辑 } finally { lock.unlock(); }

3. 灵魂拷问:值得吗?