早些年我们用 Redis 实现分布式锁就是SET key value NX EX 30一把梭。直到有次库存扣减接口里方法 A 拿了锁调方法 B方法 B 又要拿同一把锁——自家的锁把自家的线程堵死了死锁。那次我们才意识到分布式锁不是setnx 一下就完事可重入、自动续期、释放安全这三件事少一个都会出事。后来切到 Redisson它把这些坑全包了。这篇把 Redisson 的加锁链路、看门狗续期、可重入计数拆开讲并附上我们踩过的坑。一个最基础的误用锁自己锁死自己先看我们当初手撸锁翻车的样子// 错误示范不可重入的锁方法嵌套直接死锁 public void deductStock(long skuId) { String lockKey lock:stock: skuId; redis.setnx(lockKey, 1, 30); // 1. 拿到锁 try { innerAudit(skuId); // 2. 内部又去抢同一把锁 } finally { redis.del(lockKey); } } void innerAudit(long skuId) { String lockKey lock:stock: skuId; redis.setnx(lockKey, 1, 30); // 3. 同一个线程再抢 - 抢不到阻塞/失败 // ... }逐行第 1 行主方法抢到锁第 3 行内部方法用同样的 key 再抢因为原线程还持有锁、又不是可重入语义setnx返回 false要么阻塞要么直接失败。同一线程持有锁期间不能再次进入就是不可重入锁的典型死锁。Redisson 的RLock用线程标识 重入计数解决了这个。Redisson 加锁Lua 脚本保证原子Redisson 的lock()最终执行一段 Lua 脚本简化逻辑如下这段脚本在 Redis 单线程里原子执行所以判断写入设过期不会被打断// Redisson 加锁 Lua 的核心语义伪代码还原 // KEYS[1]锁名, ARGV[1]过期毫秒, ARGV[2]客户端ID:线程ID if (redis.call(exists, KEYS[1]) 0) then redis.call(hset, KEYS[1], ARGV[2], 1); // 1. 不存在则创建 hashfield客户端:线程, value1 redis.call(pexpire, KEYS[1], ARGV[1]); // 2. 设过期默认 30s return nil; end if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); // 3. 是自己持有 - 重入计数 1 redis.call(pexpire, KEYS[1], ARGV[1]); // 4. 刷新过期 return nil; end return redis.call(pttl, KEYS[1]); // 5. 别人持有 - 返回剩余 TTL抢锁失败逐行第 1 行用 hash 结构存锁field是客户端ID:线程ID这样能区分谁持有锁第 3 行如果 field 已存在说明是自己线程重入hincrby把计数加 1这就是可重入的实现——不是能不能进而是进几次都记着第 5 行返回别人持有时的剩余时间Redisson 客户端会按这个时间做自旋等待。注意它用的是 hash 而不是简单 string正是因为要存重入次数和持有者标识两个信息。看门狗业务没跑完锁先过期怎么办最坑的是锁过期时间。如果你lock(10, TimeUnit.SECONDS)设了 10 秒但业务跑了 30 秒锁到点自动释放别的线程进来你就锁失效了。Redisson 的解法是看门狗watchdog自动续期如果你没指定 leaseTime或指定为 -1它默认每 10 秒把锁续到 30 秒直到你主动 unlock。// 用法一不指定时长 - 开启看门狗自动续期 RLock lock redisson.getLock(order: orderId); lock.lock(); // 1. 没传 leaseTime默认 30s 过期 看门狗每 10s 续期 try { createOrder(orderId); // 2. 业务再久也不怕锁提前释放 } finally { lock.unlock(); // 3. 释放 - 取消看门狗、重入归零后删 key } // 用法二指定时长 - 不续期到点必释放慎用 lock.lock(10, TimeUnit.SECONDS); // 4. 明确 10s看门狗不生效逐行第 1 行lock()不传时长Redisson 启动一个后台定时任务每internalLockLeaseTime / 3即 10 秒把 TTL 刷回 30 秒第 3 行unlock()时如果重入计数归零会取消看门狗并删除锁 key。我们踩过的坑曾有一次在 finally 之外提前 return导致 unlock 没执行看门狗一直续期锁永久不释放另一个服务卡了 40 分钟。所以unlock必须进finally。第 4 行的显式时长适合业务时长可控的场景比如明确知道 10 秒内一定跑完的本地计算但凡涉及远程调用就别用远程超时你控制不了。释放锁为什么不能无脑 del释放锁最危险的一步是删了别人的锁。Redisson 的 unlock 同样用 Lua 保证原子先判断 field 是不是自己重入计数减 1减到 0 才删 key。// Redisson 解锁 Lua 的核心语义伪代码还原 // KEYS[1]锁名, ARGV[2]客户端ID:线程ID if (redis.call(hexists, KEYS[1], ARGV[2]) 0) then return nil; // 1. 不是自己的锁直接返回防误删 end local counter redis.call(hincrby, KEYS[1], ARGV[2], -1); // 2. 重入计数 -1 if (counter 0) then redis.call(pexpire, KEYS[1], ARGV[1]); // 3. 还有重入层只刷新过期 return nil; end redis.call(del, KEYS[1]); // 4. 计数归零才真正删除 return nil;逐行第 1 行先校验 field 是不是自己不是就返回彻底避免删了别人正在用的锁第 2 行重入计数减 1第 4 行只有归零才del。这段 Lua 之所以重要是因为它把判断归属 减计数 删 key三步合成原子操作中途不会被别的请求插入。对比我们手撸时代码里if (get(key).equals(myId)) del(key)——这两步不是原子的高并发下可能刚判断完、锁就被别人抢走你又把它删了等于把别人的锁解了。红锁 RedLock多节点防单点如果你的 Redis 是单实例实例挂了锁就没了。Redisson 支持 RedLock红锁向 N 个独立 Redis 节点加锁多数成功才算拿到锁容忍少数节点故障// 红锁向多个独立 Redis 节点加锁 RLock lock1 redisson1.getLock(resource); RLock lock2 redisson2.getLock(resource); RLock lock3 redisson3.getLock(resource); RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3); // 1. 聚合多节点锁 redLock.lock(); // 2. 多数(N/21)节点成功才算拿到 try { // 临界区 } finally { redLock.unlock(); }逐行第 1 行把多个独立节点的锁聚合成一个RedissonRedLock第 2 行向各节点加锁Redisson 计算拿到锁的节点数是否过半过半才算成功并记录各个节点实际耗时来校验时钟。需要说明的是RedLock 在分布式理论界有争议Martin Kleppmann 曾发文质疑其时钟假设它不是银弹。我的取舍默认看门狗别乱指定时长我的观点生产环境 90% 的场景用lock()不指定 leaseTime让看门狗兜底除非你能 100% 保证业务耗时上限且小于你设的时长。我们曾因为怕锁一直不释放而给所有锁显式设了 15 秒结果一个依赖第三方支付的接口平均 12 秒、偶尔 20 秒锁在 15 秒提前释放造成超卖——这是典型的为防一个坑踩了另一个坑。另外RedLock 我不太建议作为默认它带来的部署复杂度和理论争议在大多数业务里换不来对等的收益单实例 Redis 配看门狗 业务幂等反而更稳。真正该做的是锁的粒度要细按 orderId 而不是全局锁unlock 必须进 finally临界区里别再调会长时间阻塞的远程服务。还有一个我们交过学费的细节别把lock()放在 for 循环里对一批 orderId 逐个加锁那样可能死锁A 拿了 1 等 2B 拿了 2 等 1。要对一批资源加锁要么按固定顺序先小 id 后大 id依次获取要么用RedissonMultiLock一次性锁多个。另外锁的自动续期只在你用lock()不指定时长时生效一旦你为了防死锁显式传了lock(15, SECONDS)看门狗就关闭业务超时锁照常释放——这两条规则记反了线上迟早出事。我们后来统一封装了一个DistributedLockTemplate强制所有加锁走模板、unlock 写进模板的 finally从根本上消除了漏 unlock和乱设时长两类人为错误。思考题假设你的加锁业务里会调用一个平均 800ms、P99 5s 的外部 HTTP 接口你用lock()默认看门狗30s/续期间隔10s会不会出问题如果把这个接口换成可能阻塞 60 秒的数据库大查询呢锁的设计要不要因此调整写在最后Redisson 的分布式锁比手撸setnx强在哪三件事Lua 脚本保证加锁/解锁原子、hash 结构实现可重入、看门狗在业务没跑完时自动续期。我们当初翻车是因为锁不可重入 释放不原子换到 Redisson 后这两类问题基本消失。但锁不是万能的——粒度、finally 释放、临界区耗时这些 Redisson 替你管不了得自己在写代码时想清楚。