从SETNX到Redisson:分布式锁演进与踩坑实战解析

发布时间:2026/9/16 9:17:49
从SETNX到Redisson:分布式锁演进与踩坑实战解析
先讲个场景线上支付回调接口突然超时客户端自动重试结果同一笔订单被重复处理用户被扣了两笔钱。你火急火燎地翻代码发现业务逻辑根本没做防重。这时候大多数人的第一反应就是——加个分布式锁。于是你打开了搜索引擎输入“分布式锁”满屏的答案几乎都在说用 Redis 的SETNX。但等你真把SETNX用上去才发现坑远比你想象的深。锁失效、锁误删、死锁、锁冲突、主从切换丢锁……每一个坑都能让你在凌晨三点盯着日志怀疑人生。这篇文章不做任何铺垫直接把我从SETNX一路走到Redisson的完整演进过程、踩过的坑、填坑的方法、以及填坑之后的思考全部整理出来。涉及代码、命令、排查思路都是可以直接拿去用的。适合被分布式锁坑过的后端开发也适合正在准备面试、想把这套逻辑彻底讲清楚的同学。1. 为什么一定要用分布式锁1.1 单机锁为什么解决不了问题我先问你一个问题一段普通的synchronized代码块在单机环境下能不能防住重复提交答案是可以。因为在同一个 JVM 进程里synchronized锁的是对象监视器所有线程共享这个监视器谁拿到锁谁就执行逻辑上没有问题。但一旦系统部署到多个节点比如两台服务器同时跑同一个服务前面放一个负载均衡同一时刻两个请求分别打到不同节点两个节点各自执行各自的synchronized锁根本不管用。因为节点 A 的锁管不住节点 B 的线程跨 JVM 的互斥单机锁天然做不到。这种场景在业务里太常见了定时任务在多个实例上同时启动导致任务重复执行用户快速点击提交按钮请求被负载均衡到不同节点导致订单重复创建消息队列的消费者做了手动 ACK 但业务处理超时消息被重新投递消费逻辑被重复执行。这一系列问题的本质是需要一把“全局统一”的锁让所有节点上的所有线程都遵守同一个互斥规则。这就是分布式锁存在的原因。1.2 Redis 做分布式锁的前置基础为什么分布式锁的教程十篇里有九篇都在讲 Redis因为 Redis 有几个天然适合做锁的特点。第一个特点是单线程执行模型。Redis 的所有命令都是串行执行的不存在多个命令同时修改一个 key 的并发问题。SETNX这个命令在 Redis 内部就是一个原子操作不存在“检查 key 是否存在”和“写入 key”之间被插队的可能。这一点非常关键因为分布式锁的第一要求就是原子性。第二个特点是高性能。锁操作本身的延迟通常在毫秒甚至亚毫秒级别对业务代码的影响非常小。相比 ZooKeeper 的半同步复制协议或者数据库锁的行锁竞争Redis 在性能上优势明显。第三个特点是部署简单、生态成熟。Redis 本身就是缓存场景的标配组件很多项目已经引入了 Redis顺手用它做分布式锁不用再额外引入一套新组件运维成本和学习成本都低。1.3 分布式锁的四条底线在动手写代码之前我先给你一个评价分布式锁方案的标准框架。后面所有的方案、所有的演进你都可以拿这四条底线去衡量。我也建议你把这份框架背下来面试的时候拿出来讲比单纯背面试题有说服力得多。第一条互斥性。同一时刻只能有一个客户端持锁。这是锁存在的唯一意义如果做不到互斥后面三条都不用聊了。第二条安全性。加锁和解锁必须是同一个客户端不能出现 A 加的锁被 B 释放的情况。如果锁可以被别人乱删那锁就形同虚设。第三条可用性。不能因为锁服务本身的故障或者某个客户端持锁后崩溃导致整个系统死锁。也就是说锁一定要有自动释放机制不能无限期持有。第四条容错性。当 Redis 集群里的主节点发生故障时锁不能大面积失效或者丢失否则一旦出现脑裂锁的互斥性就会被打破。很多团队在选型分布式锁时只关注前三条忽略第四条。事后出了问题才发现所谓的高可用架构在极端情况下连最基本的互斥都保证不了。这条线我们先埋在这里后面讲主从切换和 RedLock 的时候再展开。2. 从 SETNX 写第一版锁开始2.1 第一版SETNX 加锁、DEL 解锁先回到最原始的方案用SETNX加锁。SETNX是 “SET if Not eXists” 的缩写含义是如果 key 不存在就设置 key 并返回 1如果 key 已经存在什么都不做并返回 0。# 尝试获取锁key 为锁的名称 SETNX order_lock 1第二行代码告诉你的信息是“我尝试持有锁如果返回 1说明之前没人持有这把锁我拿到了如果返回 0说明锁已经被别人占着我抢不到。”解锁就更简单直接删除这个 key 就行DEL order_lock这段原始逻辑写成 Java 代码大概是这样的// 伪代码千万别直接抄 Boolean locked redisTemplate.opsForValue() .setIfAbsent(order_lock, 1); if (locked) { try { // 处理业务逻辑 doBusiness(); } finally { redisTemplate.delete(order_lock); } }第一版方案写出来之后看起来逻辑没有任何问题。加锁、执行业务、释放锁SETNX和DEL都是原子命令锁的获取和释放都靠 Redis 完成多节点之间天然互斥。但如果只写到这一步这个锁只能算玩具。最大的问题是如果业务代码在try块里抛了异常或者服务进程直接崩溃finally里的DEL根本没有机会执行。这个锁 key 会永远存在 Redis 里其他所有客户端再也拿不到锁整个业务直接进入死锁状态。你可能想说finally里都写删除逻辑了异常不也会走 finally 吗对普通异常确实会走但进程宕机、网络闪断、机房断电不会。只要锁 key 没有过期时间死锁是必然的。这个坑我在刚写分布式锁的时候踩过一次很经典服务发布重启之后所有定时任务全部停摆排查到最后发现是一个没有设置过期时间的锁 key 一直躺在 Redis 里。2.2 第二版给锁加过期时间既然 key 可能永远不被删除那最直接的办法就是给 key 设置一个过期时间。在这个思路上你很快会写出这样一段代码Boolean locked redisTemplate.opsForValue() .setIfAbsent(order_lock, 1, 30, TimeUnit.SECONDS);注意这里我直接把过期时间作为参数传给了setIfAbsent底层对应的是SET key value EX seconds NX命令。这条命令的含义是仅当 key 不存在时写入并同时设置 30 秒过期时间。关键是设置 key 和设置过期时间这两个操作必须放在同一条命令里。这也是第二版方案最容易踩的坑之一。我见过有人分两步写Boolean locked redisTemplate.opsForValue().setIfAbsent(order_lock, 1); if (locked) { redisTemplate.expire(order_lock, 30, TimeUnit.SECONDS); }先SETNX再EXPIRE两个命令分开执行。这个写法为什么有问题因为SETNX执行之后、EXPIRE执行之前如果服务进程突然宕机这个 key 依然没有过期时间死锁的坑原样保留。Redis 命令是原子的但多个命令组合成的逻辑不是原子的。所以给锁设置过期时间这件事必须和加锁操作绑定在同一个原子操作里。第二版方案勉强可以上线了但它的潜在问题很快会暴露出来如果业务代码执行超过 30 秒锁自动过期了另一个客户端就能拿到锁并行执行相同业务互斥性失效。这个问题我们先标记一下后面 Redisson 用看门狗机制专门解决了它。2.3 第三版给锁添加上下文标识第二版方案还有一个更大的坑如果业务 A 的锁因为过期时间到了而自动释放业务 A 还没执行完业务 B 拿到了锁开始执行此时业务 A 执行完了它finally里的删除操作会把业务 B 的锁删掉业务 C 就能拿到锁三个业务一起跑锁完全失去了意义。这个场景在真实的线上环境非常常见。你不能假设你的业务永远比设置的过期时间快更不能假设两个线程不会同时在临界区执行。解决思路是加锁的时候不只是设置一个固定值而是设置一个当前客户端的唯一标识比如UUID。解锁之前先检查锁里的标识是不是自己的是才删不是就不删。String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(order_lock, requestId, 30, TimeUnit.SECONDS); if (locked) { try { doBusiness(); } finally { // 先比较再删除 String value (String) redisTemplate.opsForValue().get(order_lock); if (requestId.equals(value)) { redisTemplate.delete(order_lock); } } }写成这样之后发现新问题了吗注意finally里的两个操作先GET比较值再DEL删除 key。这中间存在一个极短的窗口。假设线程 A 执行完GET发现锁是自己的还没来得及执行DEL锁因为超时自动过期了。此时线程 B 立刻加锁成功并且拿到了同一把锁。这时候线程 A 的DEL执行了删掉的是线程 B 的锁。这就是经典的“检查再删除不是原子操作”的坑。两个操作之间虽然没有业务代码但 Redis 命令之间是可以被其他客户端的命令插入的。2.4 用 Lua 脚本解决解锁原子性要保证“检查再删除”的原子性不能靠 Java 代码的组合来实现必须借助 Redis 的 Lua 脚本。Redis 从 2.6 版本开始支持 Lua 脚本Lua 脚本在 Redis 服务端执行时脚本内部的多个 Redis 命令相当于一个完整的原子操作执行期间不会被其他命令插队。-- 解锁脚本先比较 value相同才删除 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endJava 侧对应的执行逻辑是String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(order_lock, requestId, 30, TimeUnit.SECONDS); if (locked) { try { doBusiness(); } finally { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(order_lock), requestId); } }到这里基于SETNX的分布式锁方案在单机 Redis 上基本达到了“够用”的状态加锁是原子的过期时间兜底防死锁解锁有唯一标识兜底防误删解锁操作也有 Lua 脚本保证原子性。但注意我说的是“单机 Redis 上够用”。一旦你的 Redis 上了主从架构甚至哨兵架构新的问题就来了。这也是网络上大量面试帖里“Redis 分布式锁有哪些坑”的核心考点主节点挂了锁丢了怎么办。3. Redisson 登场替你把坑填平3.1 为什么项目中最终都转向 Redisson我自己用原生SETNX Lua 脚本写了很长一段时间分布式锁直到后来接手的一个项目里已经有了 Redisson 依赖我便顺手把锁切了过去。用完第一周我的感受是以前觉得 SETNX 方案已经够用了是我没经历够那些极端场景。Redisson 是 Redis 官方推荐的 Java 客户端之一它在 Redis 基础命令之上封装了大量分布式数据结构其中就包括我们最关心的分布式锁。Redisson 的分布式锁并不仅仅是把SETNX包装成一个方法它把锁的实现、自动续期、可重入、等待机制、公平性全面做了工程化。也就是说你在SETNX方案中手动处理的那些坑它内部已经替你处理好了。举几个具体例子SETNX方案里我们需要自己设置过期时间还要每天祈祷业务不要超过这个时间Redisson 有看门狗机制默认锁的有效期是 30 秒业务没执行完就会自动续期。SETNX方案天然不支持可重入同一个线程多次进入同一个锁第二次就会被阻塞Redisson 的锁是可重入锁底层用 Redis 的 Hash 结构记录重入次数。SETNX方案里加锁失败后是立即返回还是循环重试完全靠业务自己控制Redisson 提供了tryLock和lock两个方法分别对应非阻塞获取和阻塞获取还支持等待时间参数。3.2 看门狗机制自动续期背后的设计与代价这是我用 Redisson 之后觉得最爽的功能。过去用SETNX方案时我总要预估业务最长执行时间设置一个听起来“比较安全”的过期时间。但业务的实际情况是非常难预估的偶尔一个慢 SQL 能跑好几十秒下游接口偶发超时也会拖垮整体耗时。过期时间设短了锁提前释放并发问题复现设长了万一持有锁的客户端宕机其他客户端要等很久才能抢到锁。Redisson 的看门狗机制解决了这个问题。核心思路是拿到锁之后后台起一个定时任务每隔锁有效期的三分之一时间就刷新一次过期时间。默认锁的lockWatchdogTimeout是 30 秒也就是每 10 秒刷新一次刷新操作会把锁的过期时间重新设为 30 秒。这样只要持有锁的客户端还在运行锁就不会过期。而一旦持有锁的客户端宕机看门狗线程跟着进程一起消失没有人续期锁会在最多 30 秒后自动释放。前端代码很简单直接用lock方法即可不需要传递过期时间参数看门狗就会自动生效RLock lock redissonClient.getLock(order_lock); lock.lock(); // 使用看门狗自动续期 try { doBusiness(); } finally { lock.unlock(); }如果你显式传入锁的持有时间比如lock.lock(10, TimeUnit.SECONDS)看门狗机制就不会生效因为此时你已经明确告诉 Redisson10 秒之后锁必须释放不需要自动续期。这个细节很多人不知道但面试官很喜欢问。看门狗这个设计虽然优雅用的时候也要保持清醒它是“以时间换安全”。为了让锁不提前释放Redis 里会有一个 key 被反复重置过期时间如果业务线程一直不结束这个 key 就会一直存在。所以千万不要在持有锁的情况下做长时间阻塞操作比如Thread.sleep很久否则锁实际上等于没有过期时间其他线程会被一直阻塞。看门狗解决的是“业务执行时间超过预设过期时间”的问题不是让你无限期持锁的理由。3.3 可重入锁用一个 Hash 解决的不只是重入Redisson 的锁叫做可重入锁这不是一个可有可无的加分项而是实际业务里的刚需。什么叫可重入举个例子你写了一个加锁的方法doPay()这个方法内部又调用了另一个加锁的方法doRefund()。如果锁不可重入第二个方法尝试获取同一把锁时会直接失败因为本身已经持有锁了但是锁不知道你是持有者。这就很尴尬。Redisson 底层用 Redis 的 Hash 数据结构存储锁信息。Hash 的 key 是锁名称Hash 的 field 是持有锁的线程标识客户端 ID 线程 IDHash 的 value 是重入次数。RLock lock redissonClient.getLock(order_lock); lock.lock(); try { lock.lock(); // 同一线程再次获取同一把锁重入成功 System.out.println(重入成功); lock.unlock(); } finally { lock.unlock(); }这段代码在 Redisson 里不会死锁可以正常执行。每次加锁时重入次数加 1每次解锁时重入次数减 1减到 0 的时候才真正删除锁的 key。我第一次看这个设计时比较佩服因为这意味着 Redis 的一个 Hash 结构里既保存了“谁持有锁”又保存了“持有几次”一次命令操作搞定所有语义非常巧妙。但反过来用了 Redisson 之后释放锁的次数也必须和获取锁的次数保持一致。如果你lock()了两次释放只调用了一次unlock()重入次数不会归零锁就不会真正释放其他线程永远拿不到锁。实际写代码时我推荐在finally里用lock.unlock()配合lock.isHeldByCurrentThread()做保护避免一个线程去释放另一个线程持有的锁。RLock lock redissonClient.getLock(order_lock); lock.lock(); try { doBusiness(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }3.4 公平锁、读写锁这些进阶能力Redisson 除了最基础的RLock还提供了FairLock公平锁、ReadWriteLock读写锁、Semaphore信号量、CountDownLatch闭锁等分布式版本的数据结构。其中公平锁和读写锁在业务里用得相对较多。公平锁解决的是“线程饥饿”问题。在极端情况下某个线程反复抢不到锁一直阻塞等待。Redisson 的公平锁底层使用了 Redis 的 ZSet 和 List 结构让加锁请求按照先后顺序排队先到先得避免后到的请求反复抢到锁。公平锁代码写法RLock fairLock redissonClient.getFairLock(order_lock);读写锁的思路是读锁是共享锁多个线程可以同时持有写锁是排他锁只能一个线程持有。适合“读多写少”的场景比如配置中心的配置读取和更新。用ReadWriteLock对象分别获取读锁和写锁RReadWriteLock rwLock redissonClient.getReadWriteLock(config_lock); RLock readLock rwLock.readLock(); RLock writeLock rwLock.writeLock();我建议普通业务先用最基本的RLock就好公平锁和读写锁都是有明确场景才上不要为了用新功能而引入复杂度。毕竟分布式环境下排队机制本身也有性能损耗。3.5 RedLock 是怎么回事讲到分布式锁绕不开 Redis 作者提出的 RedLock 算法。RedLock 的基本思想是不信任单个 Redis 节点同时向多个独立的 Redis 节点发起加锁请求只要超过半数N/21的节点加锁成功就认为加锁成功解锁时向所有节点发起解锁请求。Redisson 里也有对应的实现叫做RedissonRedLock。使用方式是把多个节点的 RLock 聚合起来RLock lock1 redissonClient1.getLock(order_lock); RLock lock2 redissonClient2.getLock(order_lock); RLock lock3 redissonClient3.getLock(order_lock); RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3);但说实话我对 RedLock 的态度一直比较保守。RedLock 能解决数据不一致的场景但它引入了更大的复杂度需要部署多个独立的 Redis 实例加锁时间变长运维成本上升而且业界对 RedLock 的正确性也有不少争论。核心争议在于RedLock 依然是基于 AP 模型的 Redis而不是 CP 模型的组件它无法完全保证在极端网络分区情况下的互斥性。我的建议很简单如果你的业务对锁的正确性要求极高比如涉及资金操作、库存扣减直接上 ZooKeeper 或 etcd 这类 CP 模型组件它们用强一致性协议保证锁的互斥性。如果业务允许 Redis 分布式锁偶尔出现极端情况下的锁失效那单机 Redis 哨兵已经能覆盖绝大多数场景没必要硬上 RedLock。现实中的高并发系统没有哪个方案是银弹只有适合不适合。4. 常见问题与排查技巧实录4.1 业务没过期时间就崩了这是我在线上实际遇到过的第一类问题。用SETNX方案时我们给锁设置了 30 秒过期时间结果某个业务方法因为调用第三方接口超时整体执行了将近 50 秒。锁在第 30 秒就自动释放了另一个请求立刻拿到锁两边同时处理同一笔订单。排查方法一般分三步。第一步看日志里有没有两个线程同时进入临界区的记录可以通过加锁操作前后输出线程 ID 或自定义 traceId 来判断。第二步检查业务的执行耗时如果发现确实经常超过过期时间说明过期时间设置不合理或者依赖的下游服务不稳定。第三步升级方案换成 Redisson 的看门狗让锁的过期时间随业务执行动态调整。这里也顺带说一下使用SETNX方案时合理的过期时间应该怎么设置。核心原则是过期时间要大于业务执行的最大耗时而不是平均耗时。如果业务偶尔会慢可以考虑设置一个相对大的值比如 5 倍于平均耗时如果你对耗时实在没有把握就换 Redisson。为了省一个依赖而每天担心锁提前失效我认为这笔账不划算。4.2 锁被误删了锁被误删几乎是每个写分布式锁的人都会遇到的坑。我也经历过线上突然出现重复数据排查日志发现临界区同时有两个线程在执行再查 Redis 发现锁 key 被人为删除了但根本没有第二个客户端显式调用过DEL。原因在上面已经分析过加锁和解锁之间如果只用充当标识的普通字符串且解锁时“先 GET 比较再 DEL”不是原子操作就有可能删掉别人的锁。解决方法是加锁时用唯一 ID 作为 value解锁时用 Lua 脚本原子完成比较和删除。另外一个容易忽视的坑是如果用tryLock加锁失败代码里没有释放锁的finally分支还会导致锁永远无法释放。正确的写法是不管加锁是否成功释放锁的代码都应该在finally中按需调用并且释放前判断当前线程是否持有锁。4.3 主从切换导致锁失效这是 Redis 分布式锁最麻烦的一类问题我用一个真实案例说明。某次线上 Redis 主节点因为机器负载过高触发哨兵切换新的主节点选举完成之后业务出现了一波并发异常。原因是这样客户端 A 在旧主节点上成功写入锁 key此时锁的数据还没来得及同步到从节点主节点就宕机了。哨兵把从节点提升为主节点后新的主节点里没有这个锁 key。客户端 B 尝试加锁发现 key 不存在加锁成功。此时客户端 A 和客户端 B 同时认为自己持有锁。这就是前面提到的第四条底线“容错性”的问题。解决思路有三类第一类把锁和 Redis 的持久化策略结合起来比如开启 AOF 并且设置合理的刷盘策略尽量降低主从切换时锁数据丢失的概率。第二类用强一致组件替代 Redis比如 ZooKeeper 或 etcd从架构层面解决。第三类使用 RedLock 算法但它增加复杂度而且不能彻底解决。就我个人的经验而言绝大多数业务其实不需要和后三类方案死磕。如果你的锁是用来防重复提交、防止缓存击穿这类可以容忍极端场景下偶发并发的问题Redis 分布式锁完全够用。但如果涉及资金、库存这种绝对不能出错的场景我更推荐在业务层再加一条兜底比如数据库唯一索引、乐观锁版本号而不是单纯依赖分布式锁。4.4 排查时常用的工具和命令不管用SETNX还是 Redisson排查过程中总得和 Redis 打交道。我先说说上一个项目里用的排查工具大部分后端开发应该都用得上。第一个排查方式就是直接用命令行。连接上 Redis 之后最常用的排查命令是查锁 key 的剩余过期时间、查锁 key 对应的 value以及看锁 key 的类型# 查看锁是否存在以及锁的值 GET order_lock # 查看锁的剩余过期时间单位秒 TTL order_lock # 查看锁 key 的类型Redisson 默认用 hash 存锁 TYPE order_lock # 查看 hash 结构里的字段和值用于观察 Redisson 锁的重入次数 HGETALL order_lock排查之前要留意一下当前连的是哪个 Redis 节点如果是主从架构主节点和从节点都能响应查询但只有主节点能处理写入请求。别查了半天发现查的是从节点锁 key 还没同步过来容易误导判断。第二个工具是可视化客户端我用得最多的是 Another Redis Desktop Manager。它支持连接多个 Redis 实例可以方便地浏览 key、查看 value、执行命令排查锁 key 是否存在、过期时间还剩多少秒都直观很多。网上一搜一堆下载教程注意选对 Windows 或 macOS 对应的版本。还有一个实操层面的忠告在排查分布式锁问题时尽量把每次加锁、解锁、加锁失败的日志都带上 key 名称和线程标识。在SETNX方案时代我曾在关键方法上加了这些日志后来排查问题时节约了大量时间。Redisson 也支持自定义监听器可以拿到锁的获取和释放事件有日志的情况下事件驱动排查比瞎猜高效得多。5. 分布式锁的选型与最佳实践5.1 三种分布式锁方案的选型对比把方案从基础到复杂排个序大概是原生SETNX Lua Redisson ZooKeeper/etcd。没有绝对的优劣只有适不适合。我用一个表格带你过一遍核心差异面试或者做技术选型的时候可以当参考。维度SETNX LuaRedissonZooKeeper / etcd实现成本高要自己处理续期、重入、释放低API 封装完整中需要引入新组件性能高高中协议更重自动续期无需手动设计有看门狗有临时节点机制可重入手动实现内置支持内置支持可靠性依赖 Redis 主从状态依赖 Redis 主从状态强一致更可靠适用场景学习、简单防重绝大多数业务场景资金、库存等极高一致性场景从这张表能看出Redisson 在实现成本和功能丰富度上的优势都很明显。如果想在业务代码里快速、安全地使用分布式锁我推荐优先使用 Redisson。5.2 自己设计一个“够用”的分布式锁如果你在低版本 Redis 环境或者因为各种原因不能引入 Redisson那至少要自己实现一个“够用”的锁。结合前面的演进过程我总结了几条设计原则附上对应的代码语义你可以按顺序去检查自己的实现。第一加锁必须是原子的并且要同时设置过期时间。对应命令是SET key value EX seconds NX。第二锁的 value 必须是全局唯一的能标识当前线程或请求。一般用 UUID 字符串。第三解锁必须使用 Lua 脚本先比较 value 再删除 key两步合在一个原子操作里。第四如果业务可能需要较长时间执行要设计续期机制。可以自己写一个定时任务每隔一段时间重置过期时间要求是在持有锁期间定时任务不能停锁释放后要取消定时任务。第五考虑可重入。如果业务中可能出现同一个线程多次获取同一把锁的场景需要像 Redisson 一样用 Hash 存重入次数或者规定业务不得嵌套加锁。做到这五条虽然还是不如 Redisson 完整但至少能满足大多数业务的诉求。再强调一次能用现成库解决的就不要重复造轮子分布式锁这种深度耦合并发语义的东西Redisson 的成熟度不是十天半个月能赶上的。5.3 从 SETNX 到 Redisson核心演进带来的启示回顾整个演进过程从SETNX到Redisson其实解决的并不是“命令不同”的问题而是“从命令到框架”的工程化问题。SETNX只是 Redis 提供的一个原子命令它解决的是“用一个 key 实现互斥标识”这个最底层的问题。但分布式锁是一个系统问题它还需要考虑持有时间、自动续期、可重入、公平排队、异常释放、故障容错。这些不是单条命令能覆盖的语义而是需要一整套逻辑来组织命令、状态和异常处理。Redisson 能成为事实标准恰恰因为它把分布式锁从“命令”提升到了“数据结构和算法”的层面。你不再需要手动处理细节只需要调用lock()和unlock()。与此同时我也觉得不要盲目迷信新框架。Redisson 虽然解决了大部分问题但它底层还是 Redis还是建立在 AP 系统的前提之上。如果你的系统整体已经上了 ZooKeeper 或 etcd那选择对应组件的分布式锁反而更合理。技术选型不应该被“谁热门”绑架而应该被“谁更匹配当前系统的数据一致性模型”引导。5.4 一些从项目实战里攒下来的经验最后分享几个我在实际项目里踩过坑之后总结出来的经验不一定能写进教科书但绝对实用。第一个经验分布式锁不要全局复用同一个 key。锁的粒度要根据业务场景划分比如订单支付应该用“订单号”作为锁的 key而不是用一个全局pay_lock。如果把所有支付请求都压到同一把锁上系统的并发能力会被锁严重拖累。锁粒度越小并发能力越强但粒度太小又容易造成大量锁 key 堆积需要你在两者之间找平衡。第二个经验加锁的时机要准确。我见过不少代码在进入服务层之前就加了锁然后经过一堆参数校验、权限校验、日志埋点最后才走到真正的业务逻辑。锁的持有时间越长锁冲突的概率就越高其他请求等待的时间就越长。正确做法是走到真正需要互斥的核心逻辑之前才加锁逻辑执行完立刻释放。第三个经验一定要关注锁相关 metrics。可以在加锁成功、加锁失败、等待获取锁三个节点埋点统计耗时和次数。分布式锁的性能瓶颈很多时候不是 Redis 处理不过来而是业务临界区的执行时间过长导致排队严重。没有监控数据这个问题只能靠玄学排查。第四个经验分布式锁不是幂等的替代品。锁解决的是多个请求之间的互斥幂等解决的是同一个请求重复执行的问题。不能因为加了分布式锁就忽视接口的幂等设计。上线之后出过问题的人一定懂这句话的意思。第五个经验不论用哪种方案锁的 key 一定要遵循统一的命名规范。比如统一以业务名作为前缀加锁代码里就比较好排查Redis 里的 key 也一目了然。项目维护时间长了这种看起来不起眼的规范反而比代码本身更保值。第六个经验尽量通过锁的看门狗或续期机制保证锁的自动释放不要把希望全寄托在finally块上。finally保证的是代码层面的释放但服务宕机、网络分区这些场景代码根本执行不到。我做了这么多年后端一个很深的体会是分布式锁这个知识点网上讲 SETNX 的人很多讲 Redisson 用法的人也很多但真正能把为什么这么演进、每一步解决什么问题说清楚的内容很少。写完这篇文章相当于把从使用一个简单命令到依赖一个成熟框架的完整思考链路重新走了一遍。如果你正好被线上锁问题困扰或者正在准备面试需要把这块讲透希望这篇文章能帮上忙。