Redisson 利用手册:从 API 误区到看门狗失效,在此闭幕分布式锁的噩梦 [复制链接]
发表于 2026-2-4 11:33:37 | 显示全部楼层 |阅读模式
写在前面
在上一篇《分布式锁的代价与选择:为什么我们终极拥抱了Redisson?》中,我们聊到了手写 SETNX 的"茹毛饮血"期间。既然选择了 Redisson,就意味着我们已经告别了那些让人提心吊胆的死锁噩梦。
许多时间,我们以为只是调用了一个简单的 lock.lock(),但背后着实是一整套复杂的主动续期Lua 脚本原子实行发布订阅机制在岑寂支持。
这篇文章不讲虚的,我们从常用的 API 起手,一起通过生产环境的避坑实战,末了钻进底层数据布局与 Lua 源码里,把 Redisson 彻底扒个干干净净。
一、不但是 Lock 这么简单:核心 API 全景

Redisson 之以是受欢迎,是由于它把分布式锁封装成了我们最熟悉的 java.util.concurrent.locks.Lock 接口风格,极大地低沉了学习资本。但除了最根本的 lock(),另有核心功能是你必须把握的。
1. 根本那把锁:RLock

这是 90% 场景下的默认选择。它对应 Redis 底层的 Hash 布局。
  1. RLock lock = redisson.getLock("order:1001");
  2. lock.lock(); // 阻塞式等待,默认 30秒过期,自带看门狗
  3. try {
  4.    // 业务逻辑
  5. } finally {
  6.    lock.unlock();
  7. }
复制代码
2. 更聪明的锁:tryLock (⚡️保举)

在现实业务中,我们通常不渴望线程无穷死等,浪费资源。这里有两种常见姿势:
姿势 A:要等候 + 启用看门狗 (最常用)

只指定 waitTime,不指定 leaseTime。这是既想要非壅闭(或有限等候),又想要主动续期的最佳实践。
  1. // 参数1:wait time,我只愿意排队 3秒,拿不到就走人
  2. // 参数2:时间单位
  3. // 重点:没传 leaseTime,所以看门狗机制会自动生效!
  4. boolean res = lock.tryLock(3, TimeUnit.SECONDS);
  5. if (res) {
  6.    try {
  7.      // 处理业务(哪怕跑 5分钟 也不怕锁过期)
  8.    } finally {
  9.      lock.unlock();
  10.    }
  11. } else {
  12.    log.warn("抢锁失败,别挤了!");
  13. }
复制代码
姿势 B:要等候 + 主动逾期 (慎用)

指定了 leaseTime,看门狗会失效。
  1. // 参数1:wait time,排队 3秒
  2. // 参数2:lease time,上锁后 10秒 自动强制释放(注意:指定 leaseTime 会让看门狗失效!)
  3. // 参数3:时间单位
  4. boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
  5. if (res) {
  6.    try {
  7.      // 处理业务,必须保证在 10秒 内完成!
  8.    } finally {
  9.      lock.unlock();
  10.    }
  11. }
复制代码
3. 文明的列队:公平锁 FairLock

默认的锁是非公平的(Non-Fair),线程抢锁端赖 CPU 调理,谁快谁得。但如果你的业务要求"先来后到"(好比抢票列队),请务必利用公平锁。
  1. // 内部利用 Redis 的 List(作为线程等待队列)和 Hash(作为超时记录)实现
  2. RLock fairLock = redisson.getFairLock("ticket:queue");
  3. fairLock.lock();
复制代码
4. 读多写少的神器:读写锁 ReadWriteLock

这个场景太经典了:商品详情页,读的人多(10000次/秒),改库存的人少(1次/秒)。如果全互斥,性能直接崩盘。
  1. RReadWriteLock rwLock = redisson.getReadWriteLock("product:stock:101");
  2. // 读锁:多个线程可以同时加读锁,只要没有写锁
  3. rwLock.readLock().lock();
  4. // 写锁:必须等所有读锁和写锁都释放了才能加,全互斥
  5. rwLock.writeLock().lock();
复制代码
5. 联锁 MultiLock (原子性加多把锁)

偶然间我们须要同时锁定多个资源,好比"库存"和"余额",要么都锁住,要么都不锁,防止死锁
  1. RLock lock1 = redisson.getLock("lock:order");
  2. RLock lock2 = redisson.getLock("lock:stock");
  3. // 同时加锁:lock1 lock2
  4. RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2);
  5. lock.lock();
复制代码
二、扒开底层:Hash 布局与 Lua 脚本

以下源码基于 Redisson 3.16+ 版本(目宿世产环境主流版本)分析。
Redisson 为什么能实现可重入锁?为什么它比我们自己写的 SETNX 强?
答案藏在 Redis 的数据布局里。Redisson 并没有利用简单的 String 范例,而是利用了 Hash
1. Redis 里的样子

假设我们对 order:1001 加锁,Redis 里现实存储的数据长如许:
  1. KEY: order:1001
  2. TYPE: Hash
  3. # hash 对应 value 内容
  4. {
  5.     "UUID:ThreadID" : 1  # 锁的持有者 : 重入次数
  6. }
复制代码

  • KEY: 锁的名字。
  • FIELD (Key): UUID:ThreadId。这里由客户端天生的唯一 UUID 加被骗火线程 ID 拼接而成。为什么要加 UUID? 由于差别服务器上的 JVM 进程 ID 大概一样,必须通过客户端启动时天生的 UUID(ConnectionManagerId)来唯一标识一个 Redisson 实例。
  • VALUE: 1。这是重入计数器。假犹如一个线程再 lock 一次,这里酿成 2。
2. 加锁的 Lua 脚本

Redisson 为了包管一系列判定和写入是原子的,把它封装在 Lua 脚本里发给 Redis。
  1. -- KEYS[1] = 锁名称
  2. -- ARGV[1] = 过期时间 (默认 30000ms)
  3. -- ARGV[2] = 锁持有者唯一ID (UUID:ThreadId)
  4. -- 情况 1:锁根本不存在
  5. if (redis.call('exists', KEYS[1]) == 0) then
  6.     -- 创建 Hash,设置重入次数为 1
  7.     redis.call('hincrby', KEYS[1], ARGV[2], 1);
  8.     -- 设置过期时间
  9.     redis.call('pexpire', KEYS[1], ARGV[1]);
  10.     return nil; -- 返回 null 表示加锁成功
  11. end;
  12. -- 情况 2:锁存在,且持有者就是我(重入)
  13. if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
  14.     -- 重入次数 +1
  15.     redis.call('hincrby', KEYS[1], ARGV[2], 1);
  16.     -- 重新续期
  17.     redis.call('pexpire', KEYS[1], ARGV[1]);
  18.     return nil;
  19. end;
  20. -- 情况 3:锁存在,但不是我
  21. -- 返回当前锁还剩多少毫秒过期,方便客户端等待
  22. return redis.call('pttl', KEYS[1]);
复制代码
这段脚本完善表明了:

  • 原子性:这一大坨逻辑在 Redis 里是原子实行的,不会插队。
  • 可重入:通过 hexists 判定是不是自己,是的话就 hincrby。
  • 互斥性:如果既不是新锁,也不是自己的锁,直接返回剩余时间,让你可以去睡一会儿再来。
三、拆开看门狗的黑盒:源码周游

经常听说"看门狗",它到底长什么样?
着实,它本质上是一个 HashedWheelTimer(时间轮) 驱动的定时使命。
1. 启动入口

当我们调用 lock() 不传时间时,终极会走到这里:
  1. // RedissonLock.java
  2. private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
  3.     long threadId = Thread.currentThread().getId();
  4.     Long ttl = tryAcquire(leaseTime, unit, threadId);
  5.     // 如果 lock 成功,ttl 会返回 null
  6.     if (ttl == null) {
  7.         return;
  8.     }
  9.    
  10.     // 如果失败,会订阅一个 Redis Channel,等待锁释放的消息(不用死循环空转)
  11.     // ... 省略订阅逻辑
  12. }
复制代码
关键在 tryAcquireAsync 里:
  1. private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
  2.     if (leaseTime != -1) {
  3.         // 如果你传了时间,就按你的时间走,不启动看门狗
  4.         return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
  5.     }
  6.    
  7.     // 没传时间(leaseTime = -1)
  8.     // 先设置默认 30秒 过期
  9.     RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  10.    
  11.     // 加锁成功后,开启续期任务
  12.     ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
  13.         if (e == null) {
  14.            if (ttlRemaining == null) {
  15.                // 重点:启动定时续期
  16.                scheduleExpirationRenewal(threadId);
  17.            }
  18.         }
  19.     });
  20.     return ttlRemainingFuture;
  21. }
复制代码
2. 续期的无穷套娃

scheduleExpirationRenewal 终极会调用 renewExpiration:
  1. private void renewExpiration() {
  2.     // 这里的 1/3 是硬编码的规则
  3.     // 默认 lockWatchdogTimeout 是 30000ms
  4.     // 所以每 10000ms 执行一次
  5.     Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  6.         @Override
  7.         public void run(Timeout timeout) throws Exception {
  8.             
  9.             // 执行 Lua 脚本,把 ttl 重新刷回 30秒
  10.             RFuture<Boolean> future = renewExpirationAsync(threadId);
  11.             
  12.             future.onComplete((res, e) -> {
  13.                 if (res) {
  14.                     // 如果续期成功,这就形成了递归调用:自己调自己
  15.                     renewExpiration();
  16.                 }
  17.             });
  18.         }
  19.     }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  20. }
复制代码
核心逻辑总结

  • 三分之一原则:每隔锁超时时间的 1/3(默认10秒),查抄一次。
  • 无穷递归:只要查抄到锁还在,就重置逾期时间,并注册下一次查抄。
  • 存亡绑定:这个使命跑在客户端进程里,如果客户端宕机,使命克制,Redis 里的锁在 30秒 后主动逾期。
四、我在生产环境踩过的坑:避坑实战

API 谁都会调,但能避开坑的才是老司机。这六个坑,都是真金白银换来的辅导。
💣 陷阱一:美意办坏事 —— 弄死看门狗

这是新手最容易犯的错。
❌ 错误姿势
  1. // 我怕死锁,所以强行指定 10秒 过期
  2. lock.lock(10, TimeUnit.SECONDS);
  3. // 或者
  4. lock.tryLock(1, 10, TimeUnit.SECONDS);
复制代码
⚠️ 效果
Redisson 的看门狗(WatchDog)机制只有在你未指定锁逾期时间时才访问效!
一旦你手动传了 leaseTime,Redisson 就会以为你有自己的想法,不再加入。如果你的业务由于数据库卡顿跑了 15秒,第 10秒 时锁就会欺凌逾期,其他线程势如破竹,发作并发事故。
✅ 准确姿势
除非你非常确定业务能在指定时间内跑完,否则只管不要传 leaseTime,让看门狗帮你主动续期
💣 陷阱二:锁粒度太粗 —— 全服停息键

❌ 错误姿势
  1. // 所有订单共用一把锁
  2. RLock lock = redisson.getLock("LOCK_ORDER");
复制代码
⚠️ 效果
这相当于把高速公路封成了独木桥。不管有多少个用户下单,同一时间只能处理惩罚一个。性能直接归零。
✅ 准确姿势
锁的粒度越细越好。只锁谁人具体产生竞争的资源 ID。
  1. // 只锁这个订单
  2. RLock lock = redisson.getLock("order:pay:" + orderId);
复制代码
💣 陷阱三:解锁的艺术 —— 谁加的锁谁来解

❌ 错误姿势
  1. try {
  2.     // 业务逻辑
  3. } finally {
  4.     lock.unlock(); // 直接解锁
  5. }
复制代码
⚠️ 效果

  • 如果业务实行超时,锁已经被主动开释了,你再去 unlock 会抛出 IllegalMonitorStateException。
  • 如果不鉴戒解了别人的锁(固然 Redisson 有 ID 校验防止误删,但非常处理惩罚依然告急)。
✅ 准确姿势
  1. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  2.     lock.unlock();
  3. }
复制代码
💣 陷阱四:重入锁的"递归噩梦"

Redisson 的锁固然是可重入的(Reentrant),但如果你在递归或嵌套调用中不注意,很容易逻辑杂乱。
❌ 风险代码
  1. void methodA() {
  2.     lock.lock();
  3.     try {
  4.         methodB(); // methodB 里又 lock 了一次
  5.     } finally {
  6.         lock.unlock(); // 只解了一层
  7.     }
  8. }
复制代码
⚠️ 效果
Redis 里的锁计数器(Counter)如果不归零,锁是不会开释的。确保你的加锁次数息争锁次数严酷匹配
💣 陷阱五:主从切换的"幽灵锁"

这是 Redis 架构天生的短板。

  • Client A 在 Master 节点拿到了锁。
  • Master 还没来得及把锁同步给 Slave,就宕机了。
  • Slave 升级为新的 Master。
  • Client B 来加锁,发现新 Master 上没锁,于是也加锁乐成
⚠️ 效果
A 和 B 同时持有了锁。
解法:如果你不能容忍这个概率(极低),请看下文的 RedLock,大概转投 Zookeeper。对于 99% 的业务,我们选择继续这个风险。
五、RedLock 的爱恨情仇

有些口试官特殊喜欢问 RedLock,但在现实工作中,它是一个让人爱恨交加的存在。
1. 它是为了办理什么?

办理 Redis 主从集群在 Failover(故障转移)时大概丢锁的标题。
2. 怎么用?

你须要准备 3个或5个 完全独立的 Redis 实例(不是 Cluster,不是 Sentinel,就是干干净净的单实例)。
  1. RLock lock1 = redissonInstance1.getLock("lock");
  2. RLock lock2 = redissonInstance2.getLock("lock");
  3. RLock lock3 = redissonInstance3.getLock("lock");
  4. // 创建红锁
  5. RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
  6. try {
  7.     // 同时向 3个 Redis 申请锁
  8.     // 只要有 > 1.5个 (即2个) 申请成功,就算赢
  9.     lock.lock();
  10.     // 业务逻辑
  11. } finally {
  12.     lock.unlock();
  13. }
复制代码
3. 魂魄拷问:值得吗?

我的看法是:不值得

  • 运维资本飙升:为了个锁,我要多维护好几个独立的 Redis?
  • 性能打折:串行大概并发去多个节点哀求,网络开销大。
  • 并非绝对安全:Martin Kleppmann 指出,如果发生 STW(Stop-The-World)GC,大概时钟发生跳跃,RedLock 依然大概失效。
发起
如果你在做银行核心账务体系,请用 ZookeeperEtcd
除此之外的 99% 的场景,Redisson 共同主从集群 已经充足良好了。
结语
许多时间,我们在技能选型时容易陷入"既要又要"的怪圈。但软件工程的本质,就是衡量(Trade-off)。
Redisson 不是神,它只是一把被打磨得充足锋利的刀。它不能办理全部的划一性标题,但它在易用性性能可靠性之间找到了一个极佳的平衡点。
渴望这篇文章能帮你不但"会用"锁,更能"懂"锁。愿你的体系在洪峰流量下,依然固若金汤;愿你的代码,既有逻辑的骨架,又有温度的血肉。、
文章的末了,想和你多聊两句。
技能之路,经常是热闹与孤独并存。那些深夜的调试、灵光一闪的方案、另有踩坑爬起后的顿悟,如果能有人一起聊聊,该多好。
这里没有高深莫测的理论堆砌,只有我对后端开发、体系计划和工程实践的连续思考与沉淀。它更像我的数字条记本,记录着那些值得被记着的办理方案和头脑火花。
如果你以为本日的文章另有一点开导,大概单纯想找一个偕行者偶然聊聊技能、谈谈思考,那么,欢迎你来坐坐。

愿你前行路上,总有代码可写,有梦可追,也有灯火可亲。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表