尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

看门狗每 10 秒续一次期,业务却跑了 40 秒:Redisson 锁续期源码里藏着的 2 个默认陷阱

看门狗每 10 秒续一次期,业务却跑了 40 秒:Redisson 锁续期源码里藏着的 2 个默认陷阱 title: 看门狗每 10 秒续一次期业务却跑了 40 秒Redisson 锁续期源码里藏着的 2 个默认陷阱date: 2026-08-22category: 分布式锁tags: [Java, 后端, Redis, Redisson, 并发编程]我们订单服务用 Redisson 做分布式锁锁一段「扣减库存 生成订单」的逻辑。某天凌晨告警库存出现了超卖同一件商品被下了两单。查日志发现两个线程都拿到了同一把锁几乎同时进入临界区。翻到 Redisson 源码才明白问题出在「看门狗」和我们写锁的方式上——这个坑我们踩了两次才彻底搞清。事故现场两把锁同时生效那段代码最早是这样写的RLock lock redissonClient.getLock(stock: productId); // 显式指定 30 秒过期 lock.lock(30, TimeUnit.SECONDS); try { // 扣库存 建订单偶发需要 35~50 秒 orderService.create(productId); } finally { lock.unlock(); }看起来没毛病30 秒过期业务跑完释放。但orderService.create在大促时偶发要 40 秒超过了 30 秒。按我们的理解Redisson 有「看门狗」会自动续期锁不会提前失效——可事实是业务跑到 30 秒时锁真的过期了另一个线程拿到了锁。看门狗源码它只在「不指定 leaseTime」时才工作翻 Redisson 的RedissonLock#lock()和scheduleExpirationRenewal关键在这里// RedissonLock 内部续期调度简化 private void scheduleExpirationRenewal(long threadId) { // 只有 leaseTime -1即未显式指定才进入看门狗逻辑 if (this.expirationRenewalMap.containsKey(threadId)) { return; } Timeout task commandExecutor.getConnectionManager() .newTimeout(new TimerTask() { Override public void run(Timeout timeout) { // 每 internalLockLeaseTime / 3 续一次期 RFutureBoolean future renewExpirationAsync(threadId); future.onComplete((res, e) - { if (res) { // 递归续期默认 30s 租约每 10s 续一次 scheduleExpirationRenewal(threadId); } }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); }逐行讲三个关键点看门狗的触发前提是leaseTime -1。一旦你像我上面那样写了lock(30, SECONDS)Redisson 认为「你已经明确知道要锁多久」就不再启动看门狗锁就老老实实 30 秒后过期。默认租约internalLockLeaseTime是 30 秒看门狗每30 / 3 10秒续一次期靠 Lua 脚本把过期时间重新刷成 30 秒。续期也是用 Lua 保证原子pexpire只对「还持有锁」的 key 生效如果锁已被别人拿走续期静默失败不会把别人的锁续上。所以正确用法是不指定 leaseTime让它走看门狗RLock lock redissonClient.getLock(stock: productId); // 不传 leaseTime默认走看门狗每 10s 自动续期 lock.lock(); try { orderService.create(productId); // 跑多久都行锁不会提前掉 } finally { // 释放时 Redisson 会取消看门狗定时任务 lock.unlock(); }第二个陷阱unlock 不在 finally 里看门狗线程永远不取消我们的第二版又踩了另一个坑有同学把unlock()写在业务逻辑后面没包在finally里。业务抛异常时unlock没执行看门狗还在每 10 秒续期这把锁成了永久锁直到 Redis key 手动删掉。Redisson 在unlock()内部会调用cancelExpirationRenewal取消续期任务// RedissonLock#unlock 内部简化 public void unlock() { // 释放锁的 Luadel key 并发布解锁消息 RFutureBoolean future unlockAsync(); future.onComplete((opStatus, e) - { // 关键取消看门狗的定时续期 cancelExpirationRenewal(); }); }所以unlock必须在finally块里确保无论业务成功还是抛异常都能执行续期任务才能被正确取消。我们后来统一封装了一层模板方法public T T withLock(String key, SupplierT action) { RLock lock redissonClient.getLock(key); lock.lock(); // 走看门狗 try { return action.get(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }isHeldByCurrentThread()这个判断很重要如果锁已经因为超时显式 leaseTime 场景被 Redis 清掉再unlock会抛IllegalMonitorState异常。加了判断超时不释放的锁就不会在 finally 里再炸一次。第三个容易被忽略的点tryLock 的等待与续期很多人用tryLock(waitTime, leaseTime, unit)来「抢不到就放弃」这里面的坑是只要传了 leaseTime看门狗同样不工作。// 想要「抢不到等 3 秒、抢到后自动续期」要分两步写 boolean locked lock.tryLock(3, TimeUnit.SECONDS); // 第一参是等待时间不传 leaseTime if (locked) { try { orderService.create(productId); // 看门狗生效不怕业务慢 } finally { lock.unlock(); } }注意tryLock(3, SECONDS)这种单参写法和tryLock(3, 30, SECONDS)完全不同前者只设等待时间、leaseTime 仍是 -1看门狗生效后者设了等待时间也设了 leaseTime30看门狗失效。少写一个参数的差别就是锁会不会提前掉。看门狗续期失败的边界看门狗不是万能的。它依赖「续期 Lua 能成功执行」如果那 10 秒窗口里 Redis 主节点宕机、或网络抖动导致续期失败锁还是会过期。我们遇到过一次 Redis 网络分区续期连续两次失败锁在 30 秒后过期业务又出现了短暂的并发重入。这也是为什么 Redisson 官方反复强调分布式锁在极端网络异常下做不到绝对互斥它只能把「误释放」的概率压到很低不能完全消除。如果你要的是「绝对不能重复执行」得在业务层再补一道幂等比如用请求唯一 ID 去重表而不是把所有信任都押在锁上。锁的粒度比锁的实现更影响吞吐踩完超卖坑之后我们才意识到锁的 key 设计比「用哪种锁」更影响性能。最初我们getLock(createOrder)锁整个下单入口结果不同用户的下单请求被串行化吞吐量掉了一半。改成getLock(stock: productId)只锁单个商品后不同商品之间完全不互斥。进一步我们把一个大库存拆成多个子库存行比如按仓库维度热点商品的锁竞争又降了一截。锁的命名空间要精确到「真正会冲突的资源」而不是笼统的业务方法名——这一条对 Redisson、Redis 手写锁、ZooKeeper 锁都成立。锁太粗等于自己给自己做限流锁太细又可能出现「该互斥的没互斥」这个度要靠压测数据来定不是拍脑袋。我们曾把一个粗粒度锁锁整个商家拆成细粒度锁单个商品下单接口 P99 从 320ms 降到 140ms这个数据比任何理论都直观——锁粒度优化带来的吞吐提升经常比换锁实现更明显。几个方案对比写法看门狗是否生效风险lock()无参生效每 10s 续期业务永久卡住会一直续需 finally 释放lock(30, SECONDS)不生效业务超 30s 锁提前掉并发重入tryLock(3, 30, SECONDS)不生效同上且抢不到直接返回锁不在 finally 释放续期任务不取消成为永久锁直到手动清我的取舍很明确绝大多数场景用无参lock()finally释放把续期交给看门狗只有「业务执行时间有硬上限、超时就该放弃」的场景才显式指定 leaseTime并且接受它不会自动续期、需要自己控制业务耗时。把锁当「长期持有」用的同学务必要配好finally和幂等兜底别把看门狗当免死金牌。复盘数字那次超卖发生在凌晨 02:14持续约 6 分钟涉及 3 个商品共 11 笔重复下单资损约 2400 元。修复上线后我们用压测复现单热点商品 500 并发、业务耗时随机 20~50 秒连续跑 30 分钟零重复下单同时监控看门狗续期日志每把锁稳定每 10 秒打印一次renew expiration确认续期链路正常。后续我们又把锁粒度从「整个下单方法」细化到「单商品库存行」热点商品的锁竞争下降了约 70%。思考题看门狗解决了「业务慢导致锁提前过期」但引入了「业务永久卡死时锁也永久续期」。如果你的临界区里有调用第三方支付这种不可控耗时的操作你会怎么设计锁的持有策略和超时
返回列表