当前位置: 首页 > news >正文

Redisson 使用手册:从 API 误区到看门狗失效,在此终结分布式锁的噩梦

不仅是 Lock 这么简单:核心 API 全景

Redisson 之所以受欢迎,是因为它把分布式锁封装成了我们最熟悉的java.util.concurrent.locks.Lock接口风格,极大地降低了学习成本。但除了最基础的lock(),还有核心功能是你必须掌握的。

1. 基础那把锁:RLock

这是90% 场景下的默认选择。它对应 Redis 底层的Hash结构。

RLock lock = redisson.getLock("order:1001"); lock.lock(); // 阻塞式等待,默认 30秒过期,自带看门狗 try { // 业务逻辑 } finally { lock.unlock(); }

2. 更聪明的锁:tryLock(⚡️推荐)

在实际业务中,我们往往不希望线程无限死等,浪费资源。这里有两种常见姿势:

姿势 A:要等待 + 启用看门狗 (最常用)

只指定waitTime,不指定leaseTime。这是既想要非阻塞(或有限等待),又想要自动续期的最佳实践。

// 参数1:wait time,我只愿意排队 3秒,拿不到就走人 // 参数2:时间单位 // 重点:没传 leaseTime,所以看门狗机制会自动生效! boolean res = lock.tryLock(3, TimeUnit.SECONDS); if (res) { try { // 处理业务(哪怕跑 5分钟 也不怕锁过期) } finally { lock.unlock(); } } else { log.warn("抢锁失败,别挤了!"); }
姿势 B:要等待 + 自动过期 (慎用)

指定了leaseTime,看门狗会失效。

// 参数1:wait time,排队 3秒 // 参数2:lease time,上锁后 10秒 自动强制释放(注意:指定 leaseTime 会让看门狗失效!) // 参数3:时间单位 boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS); if (res) { try { // 处理业务,必须保证在 10秒 内完成! } finally { lock.unlock(); } }

3. 文明的排队:公平锁FairLock

默认的锁是非公平的(Non-Fair),线程抢锁全靠 CPU 调度,谁快谁得。但如果你的业务要求"先来后到"(比如抢票排队),请务必使用公平锁。

// 内部利用 Redis 的 List(作为线程等待队列)和 Hash(作为超时记录)实现 RLock fairLock = redisson.getFairLock("ticket:queue"); fairLock.lock();

4. 读多写少的神器:读写锁ReadWriteLock

这个场景太经典了:商品详情页,读的人多(10000次/秒),改库存的人少(1次/秒)。如果全互斥,性能直接崩盘。

RReadWriteLock rwLock = redisson.getReadWriteLock("product:stock:101"); // 读锁:多个线程可以同时加读锁,只要没有写锁 rwLock.readLock().lock(); // 写锁:必须等所有读锁和写锁都释放了才能加,全互斥 rwLock.writeLock().lock();

5. 联锁MultiLock(原子性加多把锁)

有时候我们需要同时锁定多个资源,比如"库存"和"余额",要么都锁住,要么都不锁,防止死锁

RLock lock1 = redisson.getLock("lock:order"); RLock lock2 = redisson.getLock("lock:stock"); // 同时加锁:lock1 lock2 RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2); lock.lock();

二、扒开底层:Hash 结构与 Lua 脚本

以下源码基于Redisson 3.16+版本(目前生产环境主流版本)分析。

Redisson 为什么能实现可重入锁?为什么它比我们自己写的 SETNX 强?
答案藏在 Redis 的数据结构里。Redisson 并没有使用简单的String类型,而是使用了Hash

1. Redis 里的样子

假设我们对order:1001加锁,Redis 里实际存储的数据长这样:

KEY: order:1001 TYPE: Hash # hash 对应 value 内容 { "UUID:ThreadID" : 1 # 锁的持有者 : 重入次数 }
  • KEY: 锁的名字。
  • FIELD(Key):UUID:ThreadId。这里由客户端生成的唯一 UUID 加上当前线程 ID 拼接而成。为什么要加 UUID?因为不同服务器上的 JVM 进程 ID 可能一样,必须通过客户端启动时生成的 UUID(ConnectionManagerId)来唯一标识一个 Redisson 实例。
  • VALUE:1。这是重入计数器。如果同一个线程再 lock 一次,这里变成 2。

2. 加锁的 Lua 脚本

Redisson 为了保证一系列判断和写入是原子的,把它封装在 Lua 脚本里发给 Redis。

-- KEYS[1] = 锁名称 -- ARGV[1] = 过期时间 (默认 30000ms) -- ARGV[2] = 锁持有者唯一ID (UUID:ThreadId) -- 情况 1:锁根本不存在 if (redis.call('exists', KEYS[1]) == 0) then -- 创建 Hash,设置重入次数为 1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; -- 返回 null 表示加锁成功 end; -- 情况 2:锁存在,且持有者就是我(重入) if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 重入次数 +1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重新续期 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 情况 3:锁存在,但不是我 -- 返回当前锁还剩多少毫秒过期,方便客户端等待 return redis.call('pttl', KEYS[1]);

这段脚本完美解释了:

  1. 原子性:这一大坨逻辑在 Redis 里是原子执行的,不会插队。
  2. 可重入:通过hexists判断是不是自己,是的话就hincrby
  3. 互斥性:如果既不是新锁,也不是自己的锁,直接返回剩余时间,让你可以去睡一会儿再来。

三、拆开看门狗的黑盒:源码漫游

经常听说"看门狗",它到底长什么样?
其实,它本质上是一个HashedWheelTimer(时间轮)驱动的定时任务。

1. 启动入口

当我们调用lock()不传时间时,最终会走到这里:

// RedissonLock.java private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = tryAcquire(leaseTime, unit, threadId); // 如果 lock 成功,ttl 会返回 null if (ttl == null) { return; } // 如果失败,会订阅一个 Redis Channel,等待锁释放的消息(不用死循环空转) // ... 省略订阅逻辑 }

关键在tryAcquireAsync里:

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1) { // 如果你传了时间,就按你的时间走,不启动看门狗 return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } // 没传时间(leaseTime = -1) // 先设置默认 30秒 过期 RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); // 加锁成功后,开启续期任务 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining == null) { // 重点:启动定时续期 scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; }
http://www.jsqmd.com/news/1101569/

相关文章:

  • Python pickle反序列化进阶:绕过R操作码黑名单与Gadget链构造
  • n8n 定时任务怎么搭? 我做了跨境选品自动化
  • GESP2026年6月认证C++三级( 第一部分选择题(8-15))精讲
  • SAP ABAP实战:手把手教你用BAPI创建销售订单时,如何绕过标准逻辑修改税额(附完整代码)
  • MATLAB手势识别GUI工程包:带全流程图像处理演示与中间结果可视化
  • GEE实战:手把手教你用BFASTmonitor算法监测ERA5雪盖变化(附完整代码与避坑指南)
  • APK Installer:Windows上最便捷的Android应用安装工具,3分钟搞定APK安装
  • VMware虚拟机迁移失败?5个致命陷阱与4步急救方案(附实测成功率98.7%脚本)
  • Android应用重打包攻击防御实战:从代码加固到Google Play Integrity API
  • 用EGO1开发板玩转FPGA串口通信:从拨码开关到数码管显示的完整流程(Vivado 2022.1)
  • AI原生开发时代已至(2025年Q1全球IDE集成率骤升68%):你还在手写CRUD吗?
  • 文献综述写得像文献堆砌?笔墨 AI 梳理研究脉络,整合最新研究动态
  • 后端开发中的6个常见性能瓶颈及解决方案
  • 制造业老板的AI转型指南:从困惑到落地,收藏这份实用路径图!
  • 终极指南:用go2rtc彻底解决多协议摄像头流媒体管理难题
  • SpringBoot+Vue3实战:手把手教你从零搭建一个毕业论文管理系统(附完整源码)
  • APK安装器:Windows原生运行安卓应用的5步革命性方案
  • 摩托罗拉 Moto Tag 2 美国上市,限时优惠!超宽带定位+500 天续航太香了
  • 省掉两个传感器!用Simulink+CarSim手把手教你估算卡车质量和坡度(附EKF模型)
  • 别再死记硬背!用Python脚本帮你自动验证Educoder离散数学自然推理系统答案
  • KMS智能激活工具终极指南:三步永久解决Windows和Office激活难题
  • 别再死记硬背SQL了!用Node.js实战项目带你玩转数据库增删改查
  • 看完LA4VLA后发现,移除视觉VLA反而学得更好。
  • SAP PS模块实战:手把手教你用BAPI批量创建WBS元素(附代码示例)
  • 用STC89C52和MFRC522模块DIY一个宿舍门禁,附完整代码和LCD12864显示
  • AI “幻觉“揭秘:小白程序员必备RAG技术,收藏学会轻松应对大模型挑战!
  • 从零搭建AI增强型CI/CD流水线:集成CodeWhisperer+自定义规则引擎的完整配置手册
  • 深入解析Java沙箱机制:从核心原理到现代应用安全实践
  • 【计算机毕业设计案例】基于 SpringBoot+Vue 的高校教师工作量化统计分析系统的设计与实现 基于 SpringBoot+Vue 的教师工作量考勤统计系统(程序+文档+讲解+定制)
  • 用STM32F0搞懂DMX512:从协议帧到驱动WS2812B的完整代码实战