发布时间:2026/8/22 16:15:47
看门狗每 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%。思考题看门狗解决了「业务慢导致锁提前过期」但引入了「业务永久卡死时锁也永久续期」。如果你的临界区里有调用第三方支付这种不可控耗时的操作你会怎么设计锁的持有策略和超时

相关新闻

2026/8/22 16:15:47

linear mixed models for ecology in R

背景:数据点不独立,有嵌套(伪重复问题)例如:同一物种被测量了多个数据;同一地点采集了多个数据常用模型:mod lmer (data , formula y ~ Fixed_Factor (Random_intercept Random_Slope | Ran…

2026/8/22 17:25:50

Python的weekday()一出手,星期几立马现原形

参与编程工作时, 处理日期以及时间属于一项常见且重要的任务, 不管是开展数据分析工作, 还是构建Web应用程序, 又或是开发自动化脚本, 我们时常都需要针对日常时间开展各种各样的操作。的的模块给我们提供了强大且灵活的工具用以处理这些任务, 当中()方法是一个极为实用的函数,…

2026/8/22 17:25:50

大厂Java面试全解析:Spring Boot与微服务架构实战

1. 面试场景还原:大厂Java技术栈的典型考察路径互联网大厂Java技术面试通常采用三轮渐进式考察机制,每一轮都有明确的侧重点。第一轮基础面会聚焦Spring Boot核心机制和JVM原理,面试官往往从"创建一个Spring Boot项目需要哪些步骤"…

2026/8/22 17:25:50

3 步启动 Sakura 模型:零基础也能用的本地高质量翻译启动器

3 步启动 Sakura 模型:零基础也能用的本地高质量翻译启动器 【免费下载链接】Sakura_Launcher_GUI Sakura模型启动器 项目地址: https://gitcode.com/gh_mirrors/sa/Sakura_Launcher_GUI Sakura 启动器是一个图形化工具,把下载、配置、启动 Sakur…

2026/8/21 13:13:49

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/21 20:14:07

工业传感器与变送器详解:序章 从物理世界到工业数据

序章 从物理世界到工业数据 ——重新认识工业传感器与变送器 工业自动化系统正变得日益复杂。今天的工业现场早已不是简单的控制回路,而是由多层技术共同构成的立体体系:PLC、DCS、SCADA、MES、工业互联网、边缘计算与人工智能。控制系统可以执行复杂算法,工业网络可以实现…

2026/8/21 15:40:01

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/21 15:40:01

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/22 1:39:53

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…