
面试的时候我经常喜欢问一句你工作这么多年有没有真刀真枪写过分布式锁答案常常是“没有”。甚至有人一脸茫然反问“我们系统好像也没用到啊”。这事挺有意思的。一个在面试题里出场率极高的技术点怎么到了实际工作里很多人都没碰过是你待的公司不行还是这东西本来就是面试造火箭说实话两种情况都有。但更真实的情况是分布式锁不是一个你天天都要写的东西它甚至不是一个“写了就有成就感”的东西。它更像保险丝平时感觉不到存在真到要用的那一刻没它就会出事。工作10年没遇到过说明你所在的业务体量、系统架构、部署方式大概率还没被逼到那个份上。而一旦你遇到就说明系统已经过了某个临界点——多实例、并发写、状态共享这三件事凑齐了。这篇文章我就把分布式锁这件事拆开讲。先聊为什么很多人遇不到再聊真正需要它的场景长什么样然后给你对比几种实现方案最后重点深挖Redis分布式锁的正确打开方式和各种坑。全文没有花架子都是我实际碰过、调过、踩过的东西。1. 为什么工作10年都没遇过分布式锁先想清楚“遇不到”背后的原因很多人把“没写过分布式锁”理解成自己技术不行其实不是。技术选型永远跟着业务形态走你遇不到它大概率是因为你的系统根本不需要它。这是架构演进的结果不是个人能力的问题。1.1 单机部署时代JVM锁就够用了如果你负责的系统还是单体架构部署在一台机器上或者只是一台机器跑了几个无状态副本、靠负载均衡转发请求那所有请求最终都会落在一个JVM进程里。这种情况下多线程并发操作共享资源用synchronized、ReentrantLock就够了。JVM锁只能锁住“当前进程内”的线程。这个“进程内”三个字既是它的边界也是它够用的原因。一台机器上不管并发多高线程再多大家共享的是同一块内存。你锁住一个对象其他线程就进不来了。此时你要锁的数据库库存字段、内存缓存、本地队列都在同一个进程里JVM锁天然罩得住。我见过不少公司业务量看着挺大但架构上就是一个几百台机器跑完全相同代码的“水平复制”模式。只要请求不涉及跨机器协同改同一个东西JVM锁就绝不出问题。问题从来不是锁不够强而是根本没有跨实例的共享资源。1.2 没被逼到那一步背后的几种典型情况聊下来我发现工作多年没接触分布式锁的人背景通常逃不出这几类业务系统是纯查询类或者写操作全部走数据库事务天然串行化。这种系统对锁的需求极低。部署方式是单机或少量实例数据库扛得住压根不需要上分布式中间件。接口设计得好把并发写操作全部收敛到MQ里变成一条消息队列的消费逻辑从而在消费者内部串行处理。团队分工太细锁逻辑被公共组件层的同事写好了业务开发只调一个接口感知不到底层实现。最后一点尤其常见。大厂很多业务开发天天调用分布式锁但内心完全不知道自己在用。一个tryLock注解贴上去锁就生效了至于底层是Redis还是ZooKeeper平时根本不看。所以“没遇到过”不等于“没用过”很可能只是你站在了锁的背面。1.3 遇到分布式锁到底意味着什么一旦你在业务里真的需要自己写一把分布式锁恭喜你的系统已经进化到新的阶段了。这个阶段通常有三个标志服务变成了多实例部署同一份代码运行在多台机器上。业务操作跨越多个实例需要修改共享资源比如数据库某一行、某个库存、某个状态。这个共享资源必须支持高并发写入且结果必须唯一。举个例子一个积分系统用户每天签到送积分。用户只签到一次但你为了高可用部署了4个实例。4个实例同时收到这个签到请求如果各查各的、各写各的库存判断就会失灵。这个场景下JVM锁锁不住另外3台机器上的线程唯一能靠的就是分布式锁——让4台机器去争同一把锁抢到的那个才允许执行签到逻辑。说白了分布式锁是“多台机器之间”的协同工具。单机时它没有存在意义。你遇不到它说明你的系统还没到多机协同那一步。这样解释是不是瞬间就释然了2. 分布式锁到底在锁什么三种典型场景的硬核拆解光说“多实例并发写共享资源”还是太抽象。我直接给你摆几个具体业务场景看完你就能对号入座。2.1 库存扣减卖超了就是事故最经典的场景就是电商库存。商品库存存在数据库里正常情况下100件库存最多卖100单。秒杀的时候一瞬间有1万个人同时抢系统分别部署在上海、北京、广州的3个机房每个机房几十台服务器。如果不加分布式锁会发生什么3台服务器同时读到库存还剩5件各自往下走扣减流程最后数据库里就变成2件、0件、-3件库存变负数超卖了。有人会说用数据库行锁select for update不就行了吗能行但性能极差一张表在秒杀期间被锁得死死的其他正常读写全部卡住。更合理的做法是先用Redis分布式锁挡住请求抢到锁的线程才允许查库存、扣库存抢不到的要么告诉用户“已售罄”要么排队稍后重试。这个场景里锁的目标是“同一商品库存条目的并发写操作”。没有锁数据就错误有了锁数据才一致。2.2 定时任务防重你在执行我就不执行很多系统都有定时任务比如凌晨2点同步订单、给用户发优惠券、生成报表。单体时代你用一个Scheduled就能搞定Spring保证同进程内任务不重复执行。但一旦多实例部署每台机器都会跑同一套定时器凌晨2点一到3个实例同时执行同一个任务数据库里被插入三份重复数据。解决方式就是分布式锁。任务开始前各实例去抢一把锁拿到锁的跑没拿到的直接跳过。等拿到锁的跑完释放其他实例这次任务周期也不会再跑因为任务已经过了执行时间。我在实际项目中遇到过更隐蔽的坑不是所有任务都在整点触发有些任务依赖上次执行时间比如“每30分钟执行一次”。如果3个实例同时抢锁抢到之后又因为执行时间过长锁提前过期了其他实例会再次抢锁造成同一时间段内两个实例同时跑同一个任务。这就是第4节要讲的锁过期问题坑特别深。2.3 幂等控制重复请求只处理一次还有一个场景是防重。用户提交订单前端手抖点了两次或者支付回调重试了多次系统必须保证同一个订单号只被处理一次。如果不加锁两个请求同时进入支付结果处理逻辑就会重复加积分、重复改订单状态、重复推送消息。锁的作用就是保证“同一订单号”的处理动作是串行的。第一次请求抢到锁处理完释放第二次请求要么抢不到锁直接丢弃要么等第一次释放后再抢到但此时订单状态已经变更二次处理时判断状态不对就自动放弃。这类场景其实算是“弱分布式锁”因为就算没有锁也能靠数据库唯一索引兜底。但唯一索引在并发高峰时会有大量冲突异常日志刷得吓人处理不好还会拖慢主库。分布式锁的价值在于把冲突提前拦在业务层减轻数据库压力。2.4 锁的本质协调状态变更的唯一性你看上面三个场景不管是库存、定时任务还是幂等本质上都在做一件事在多个并发请求中只允许一个请求去改变某个“共享状态”。库存是共享状态只能一个请求去扣。任务是共享状态只能一个实例去跑。订单是共享状态只能一个线程去处理。分布式锁的使命就是把“多节点的并发写”转化成“单节点的串行写”。通过锁所有请求先在锁里面排队谁拿到锁谁动手其他人等着或者放弃。这就是它的终极价值理解这一点你学任何分布式锁实现方案都会更有方向感。3. 分布式锁的五种实现方案大盘点不止Redis一条路锁要落地总得有实际载体。业内最常见的方案有五种我按推荐程度和使用率给你排个序顺便讲透各自优缺点。3.1 基于数据库的分布式锁简单但别指望高并发最原始的方式就是利用数据库的唯一索引。建一张锁表锁名称作为唯一键。要加锁时往表里插入一条记录插入成功代表抢到锁释放锁时删除这条记录锁冲突时插入语句会报“Duplicate entry”代表抢锁失败。这个方案的好处是零引入中间件业务库现成就有。坏处也明显性能差每秒能扛的加锁次数有限数据库成了单点挂了所有锁全部失效没有失效时间进程突然崩溃记录永远留在表里产生死锁。实际项目中我见过用for update做锁的也见过用唯一索引做锁的。必须承认写起来很爽但除了极低并发的内部系统我实在不建议在生产环境用。它最大的问题不是功能不行而是“一台数据库撑不住高并发锁请求”这跟用分布式锁的初衷背道而驰。3.2 基于ZooKeeper的分布式锁可靠但有点重ZooKeeper实现分布式锁的原理依赖它底层的ZNode节点机制。多个客户端同时在一个指定的ZNode下创建临时顺序子节点编号最小的那个节点认为自己持有锁其他节点就监听自己前一个节点是否存在等待前一个节点被删除。这个方案有个关键优势临时节点的生命周期和客户端会话绑定。客户端挂了会话断开临时节点自动消失锁自动释放不会像数据库方案那样死锁。锁的可靠性极高ZooKeeper本身靠ZAB协议保证主从数据一致只要集群一半以上节点存活锁功能就能正常工作。缺点也很实在ZooKeeper性能不强加锁和释放锁的过程涉及多轮网络通信吞吐量远低于Redis组件比较重需要额外维护一套集群。不过对一致性要求极其严格的场景比如分布式事务协调、配置中心、选主操作ZooKeeper依然是王炸方案。你面试时能说出ZooKeeper加锁的底层原理面试官会立刻觉得“这人基本功扎实”。3.3 基于etcd的分布式锁云原生时代的解题新思路etcd是近年兴起的高可用键值存储核心是Raft协议保证强一致性。分布式锁实现方式跟ZooKeeper很类似客户端向etcd写入一个带租约的key利用revision机制比较自己拿到的是不是最小revision配合watch机制监听删除事件实现锁的获取与等待。etcd性能比ZooKeeper高API更简单运维成本也更低。很多云原生项目像Kubernetes选主用的就是etcd实现的分布式锁。如果你所在团队已经有etcd集群直接用它的锁实现是个很优雅的选择。但说实话大部分公司的技术栈里Redis的存在感远强于etcd所以现实中大家还是优先用Redis。3.4 基于Redis的分布式锁性能之王使用率最高的方案Redis分布式锁是目前生产环境使用率最高的方案没有之一。原因很简单绝大多数公司都有Redis加锁只需要一条命令性能极高能轻松扛住每秒几万次的加锁请求。它的核心逻辑是利用Redis的SET命令加NX不存在才设置和EX过期时间两个参数实现原子性的“加锁设置过期时间”操作。任何依赖Redis的锁方案灵魂都在两个地方一是原子性加锁和设置过期时间必须是一次操作二是过期时间锁必须有过期机制防止持有者崩溃后锁永不释放。但Redis方案也有天然短板它依赖Redis本身的高可用。如果Redis主节点挂了而锁数据还没来得及同步到从节点从节点升级为主节点后锁就丢了其他客户端就能拿到同一把锁产生安全风险。针对这个隐患Redis作者提出了RedLock算法设计思路是向多个独立的Redis节点请求加锁超过半数成功才算加锁成功。不过RedLock在业界一直有争议我们后面细聊。3.5 方案对比与选型建议一句话帮你做决定我把五种方案放在一起做了个对比方便你根据业务场景选型实现方案性能可靠性运维复杂度典型场景数据库唯一索引低中极低低频后台任务、内部系统ZooKeeper中高高强一致性场景、分布式协调etcd中高高中云原生环境、Kubernetes选主Redis单节点极高中低高并发业务、性能敏感场景Redis RedLock高中高中对可靠性要求更高的Redis场景一句话总结业务系统首选Redis追求强一致选ZooKeeper或etcd闲着没事不推荐用数据库。选型不是越复杂越好而是越贴合场景越好。Redis方案性能极好、实现简单成为事实标准原因就是它够用且容易用。接下来我重点拆解Redis分布式锁的正确实现方式。4. Redis分布式锁深挖从setnx到一把靠谱的锁Redis分布式锁网上资料多如牛毛但真要说清楚得从一条演进路线讲起。因为锁的核心代码也就几十行但每一处细节都是血泪教训堆出来的。4.1 基础写法SET NX EX 为什么能成为标配很多早期教程会教人这样写// 错误示例两步操作中间不是原子的 Long result redisTemplate.opsForValue().setIfAbsent(key, value); if (result ! null result) { redisTemplate.expire(key, 30, TimeUnit.SECONDS); // 业务逻辑 }这段代码有两个致命问题。第一setIfAbsent和expire是两次独立的Redis操作如果执行完第一步程序就崩了Key永不过期锁就死了。第二就算不崩这两步之间也有时间窗口其他线程可能趁虚而入拿到锁。正确做法是直接用一句命令完成“加锁过期时间”// 正确示例一条命令搞定 Boolean locked redisTemplate.opsForValue().setIfAbsent(key, value, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑 } finally { // 释放锁 redisTemplate.delete(key); } }Redis从2.6.12版本开始SET命令支持NX和EX参数一次网络请求就能完成“不存在才设置”和“设置过期时间”两个动作彻底解决了原子性问题。现在几乎所有Redis客户端封装也都会支持这种写法。面试的时候只要你说出“原子性”三个字面试官就知道你不是纸上谈兵。4.2 释放锁的坑千万别误删别人的锁锁的释放比获取更容易踩坑。很多人释放锁直接delete(key)看起来没问题但细想就出大事了。假设线程A拿到锁业务执行慢锁30秒超时自动释放了。此时线程B抢到锁开始执行。就在这时线程A终于执行完了执行delete(key)把B的锁给删了。紧接着线程C也抢到锁三个线程同时执行关键业务分布式锁形同虚设。解决方案是释放锁之前先判断持有者身份。加锁时Value设置成一个唯一标识比如UUID释放锁时先比较Value是否是自己当初设置的那个值是才删除。但这里又有一个新问题“比较值”和“删除Key”也是两步操作中间同样有不一致窗口。必须用Lua脚本把两步合在一起做原子操作if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段Lua脚本的意思是拿到锁的Value如果等于当前线程的标识就删除不等于说明锁已经不是我的了不能动。Redis执行Lua脚本是原子性的所以这个操作不会被人插队。4.3 锁自动过期了怎么办看门狗机制的思路锁必然要设置过期时间否则持有者崩溃后锁永不释放。但过期时间设短了业务没执行完锁就超时了其他线程冲进来。设长了万一持有者真崩了其他线程要等很久。这是个两难问题。成熟的中间件Redisson给出了解决方案看门狗Watchdog机制。Redisson加锁后默认会启动一个后台定时任务每隔一段时间检查一次锁是否还存在如果存在就把过期时间往后延长。这样只要持有锁的线程还活着锁就永远不会提前过期线程崩溃了定时任务也跟着停了锁到时间自动释放。这就像你租了一间自习室本来计划用2小时结果临时要加钟。看门狗就是自习室管理员每30分钟来敲一次门发现你还在学习就自动帮你把时间续上发现你走了就锁门不再管。我在生产环境用Redisson确实很少遇到锁意外过期的问题但要注意看门狗只在Java语言里原生可用其他语言需要自己实现类似逻辑。4.4 Redis分布式锁到底能不能用谈谈RedLock的那场争论Redis分布式锁始终绕不开一个词RedLock。它由Redis作者antirez提出思想很简单不要只向一个Redis节点加锁而是向N/21个独立的Redis节点依次申请加锁超过半数成功才算最终成功。这套算法的提出源于对“Redis主从切换丢锁”问题的担忧。但业界对RedLock的争议从未停止。马丁·克勒普曼Martin Kleppmann专门发文章批评RedLock存在时钟跳变问题而且分布式锁本身的定位应该是“性能优化手段”而非“正确性保障”。反方则认为只要业务场景允许极端小概率错误发生RedLock就是一个可用的强一致方案。我的看法是绝大多数互联网公司根本到不了需要RedLock那个量级。普通的Redis主从哨兵模式在主节点故障切换的窗口内丢锁概率极低而业务上的数据库幂等兜底通常能自动修复。真正需要强一致的场景直接上ZooKeeper更稳妥。没必要为了面试去背RedLock的争议细节但绝对值得理解它解决的“主从切换丢锁”问题因为这直接关系到锁的可靠性。5. 手把手实现一把生产级Redis分布式锁附核心代码理论讲再多不如动手写一版。我带你把一把生产可用的Redis分布式锁完整写一遍从最简单的原子写到可重入、续期、释放全套。全程Java Redisson思路你也可以换成其他语言对应的Redis客户端。5.1 锁的API设计业务方怎么用才舒服先定义锁的接口。好的锁接口必须让业务方觉得“无感”同时保证用起来足够安全public interface DistributedLock { // 尝试获取锁带超时等待时间超出返回false boolean tryLock(String lockKey, String requestId, long waitTime, long leaseTime, TimeUnit unit); // 释放锁必须校验requestId boolean unlock(String lockKey, String requestId); }业务方的典型用法是配合try-finally结构保证锁最终一定被释放String requestId UUID.randomUUID().toString(); boolean locked lock.tryLock(order:pay:12345, requestId, 3, 10, TimeUnit.SECONDS); if (locked) { try { // 业务逻辑 doBusiness(); } finally { lock.unlock(order:pay:12345, requestId); } } else { // 获取锁超时做降级处理 throw new BizException(系统繁忙请稍后重试); }这里有几个设计细节得说清楚requestId必须每次请求都生成用于释放锁时区分身份waitTime是等待锁的时间设置太短容易误判失败太长会拖慢接口响应leaseTime是锁的自动过期时间业务预估最大执行时间得按照最坏情况设。5.2 加锁与续期的代码实现加锁的核心代码我用Redisson底层思路来写兼顾原子性和可重入性。Redis里使用Hash结构存储锁信息Key是锁名称Field是持有者的请求IDValue是重入次数public boolean tryLock(String lockKey, String requestId, long waitTime, long leaseTime, TimeUnit unit) { long waitMillis unit.toMillis(waitTime); long start System.currentTimeMillis(); // 锁的超时时间默认30秒 long expireMillis leaseTime 0 ? unit.toMillis(leaseTime) : 30000L; try { while (true) { // 使用Lua脚本原子地判断并写入锁 String luaScript if redis.call(exists, KEYS[1]) 0 or redis.call(hexists, KEYS[1], ARGV[1]) 1 then redis.call(hincrby, KEYS[1], ARGV[1], 1) redis.call(pexpire, KEYS[1], ARGV[2]) return 1 else return 0 end; Long result redisTemplate.execute(new DefaultRedisScript(luaScript, Long.class), Collections.singletonList(lockKey), requestId, String.valueOf(expireMillis)); if (result ! null result 1L) { // 加锁成功启动看门狗续期 renewExpiration(lockKey, requestId); return true; } // 加锁失败判断是否超时 if (System.currentTimeMillis() - start waitMillis) { return false; } // 让出一小段时间避免活锁 Thread.sleep(50); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } }这段代码中有三个关键点每个都是坑使用Hash结构存储是为了支持“可重入”。同一线程可以多次调用lock只要Field相同就允许重入并累加计数。JUC里的ReentrantLock重入很常见分布式锁也要具备这个能力否则嵌套调用时自己锁死自己。判断“锁不存在或者持有者是当前线程”才写入这是可重入的核心逻辑。加锁成功后立即启动续期任务每隔大约1/3的锁过期时间续一次保证业务执行期间锁不消失。5.3 看门狗续期与安全释放的完整实现看门狗的本质是后台线程定时延长Key的过期时间。我用一个定时任务实现private void renewExpiration(String lockKey, String requestId) { // 每10秒执行一次续期将锁过期时间重置为30秒 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { String luaScript if redis.call(hexists, KEYS[1], ARGV[1]) 1 then redis.call(pexpire, KEYS[1], ARGV[2]) return 1 else return 0 end; redisTemplate.execute(new DefaultRedisScript(luaScript, Long.class), Collections.singletonList(lockKey), requestId, String.valueOf(30000L)); }, 10, 10, TimeUnit.SECONDS); }这个定时任务挂在当前线程上最好用线程池管理不要每次加锁都新建否则会创建大量无用线程。生产实践上直接用Redisson会比较省心它内部已经把续期逻辑封装得很完美。释放锁时必须用Lua脚本保证“校验删除”原子操作public boolean unlock(String lockKey, String requestId) { String luaScript if redis.call(hexists, KEYS[1], ARGV[1]) 0 then return nil elseif redis.call(hincrby, KEYS[1], ARGV[1], -1) 0 then return 0 else redis.call(del, KEYS[1]) return 1 end; Long result redisTemplate.execute(new DefaultRedisScript(luaScript, Long.class), Collections.singletonList(lockKey), requestId); return result ! null result 1L; }这版释放逻辑做了重入次数处理如果值减一后还大于0说明还有外层锁只减计数不删Key减到0再删除整个Key。这样即使重入三层也能正确释放。5.4 团队实践直接用Redisson会更稳自己写锁最大的价值是理解原理。但在实际团队项目中我更推荐直接用Redisson。Redisson的RLock已经实现了可重入、看门狗、公平锁、读写锁而且经过大规模生产验证Bug少接口贴近JUC风格学习成本低。RLock lock redissonClient.getLock(order:pay:12345); boolean locked lock.tryLock(3, 10, TimeUnit.SECONDS); if (locked) { try { doBusiness(); } finally { lock.unlock(); } }使用Redisson时唯一要留意的是它默认的看门狗锁续期时间为30秒。如果你的业务阻塞超过30秒看门狗会自动续期但如果业务线程被外部服务卡死超过几分钟其他线程可能已经拿不到锁了这时候需要结合实际业务判断是否要调整锁超时时间。我在实际项目中习惯把锁超时时间设成业务最大耗时的1.5倍并额外给看门狗续期逻辑加上日志方便追踪。6. 分布式锁实战五个高频问题与排查心法即使你把锁写对了线上还是可能出现各种诡异问题。我把这些年碰到过的典型问题整理成一张速查表再逐个分析根因和解法。6.1 锁误删为什么A线程删了B线程的锁现象A线程持锁执行业务超时锁自动过期。B线程抢到锁开始执行。A线程执行完执行了delete(key)把B的锁删掉。C线程随即抢到锁A、B、C三线程同时进入临界区数据错乱。根因释放锁没有校验持有者身份。解法释放锁时必须先比对当前锁的Value是否等于自己当初设置的UUID比对和删除必须放在同一个Lua脚本里原子执行。前面已经给出了完整代码。这里再强调一遍不能先get再del两步操作之间一定有空窗。6.2 锁过期业务没跑完锁却提前释放了现象锁设置了10秒过期但业务执行了15秒锁在第10秒释放第二个线程进入两个线程同时执行同一份任务。根因锁的过期时间小于业务实际耗时。这是分布式锁最经典的“过期时间设置不合理”问题。解法一是把过期时间设成业务最大耗时的数倍但这样做要小心持有者崩溃后等待时间过长二是用看门狗动态续期让锁的过期时间跟随业务执行时间自动延长三是把业务拆短减少单次持锁时间。实际排查时先看监控里持锁线程的耗时分布再结合业务调整参数不要拍脑袋改数字。6.3 Redis主从切换丢锁主节点挂了锁就丢了现象Redis主节点上存在锁Key。主节点突发故障Redis哨兵将从节点提升为主节点。但主从同步是异步的锁的写入可能还没同步到从节点。新的主节点上没有锁其他客户端能成功加锁导致锁失效。根因Redis异步复制机制导致锁状态在不同节点间不一致。解法想彻底解决要么用RedLock算法向多个Redis节点加锁需要半数以上节点成功才算成功把“单点丢失”变成“多数派存活”要么换用强一致的ZooKeeper或etcd它们的多节点同步机制能够保证锁状态的强一致要么降低对锁可靠性的要求在业务层加幂等兜底比如数据库唯一索引、乐观锁版本号让即使锁偶尔失效也不会造成不可恢复的错误。6.4 高并发下抢锁性能差Redis扛不住怎么办现象秒杀等场景下大量请求同时抢同一把锁Redis单实例QPS达到瓶颈接口响应时间飙升。根因Redis虽然是单线程但加锁请求竞争激烈时网络请求和命令处理依然会排队加上不断重试压力成倍放大。解法常见思路有几种锁分段把一个大锁拆成多个小锁比如库存1000件拆成10段每段100件抢到其中一个段就能操作本地缓存分布式锁二层架构先用本地Lock挡住大部分请求只有少量请求去争Redis锁队列削峰把抢锁的请求改造成先进入消息队列消费者串行处理避免瞬时高峰。6.5 排查分布式锁问题的通用套路定位分布式锁问题我习惯按这个顺序来看监控锁获取成功率、持锁时间分布、等待时间分布。这些指标能帮你快速确认锁是否正常。看日志加锁、释放锁的关键日志必须完整尤其要记录锁Key、RequestId、耗时、结果。看Redis用redis-cli查锁Key的TTL和Value确认锁是否意外残留、是否被占用。看业务线程持锁线程的堆栈确认业务逻辑是否真的阻塞在某个外部调用上。我踩过一个印象深刻的坑分布式锁的Value值用了用户ID拼时间戳本意是唯一标识结果两个线程同时用同一个用户ID调用Value碰撞了后者把前者的锁删了。从那以后我要求团队所有锁的Value必须用UUID不可以用业务字段拼接。7. 关于分布式锁我最后想说的几句实话写到这里你应该已经明白工作10年没遇到过分布式锁一点都不丢人甚至说明你的系统一直活在“单机一致性”的舒适区里。但这不意味着你可以永远不学它。技术栈的演进是必然的业务体量上来了你的系统迟早会从单机走向多机从JVM锁走向分布式锁。我个人的经验是真要学分布式锁别只背面试题。拿一台Redis、写一个小的Spring Boot项目模拟多实例并发扣库存你会对锁的原子性、过期时间、可重入这些概念有完全不一样的理解。踩过几回坑之后你会比那些只看过八股文的人更能判断什么场景用Redis锁、什么场景必须上ZooKeeper。最后再分享一个小技巧给锁的Key命名时遵循“业务模块:业务ID”的模式比如order:pay:12345、stock:sku:67890。线上排查问题时一看Key就知道锁在保护什么资源。这个习惯能让你在凌晨被拉起来处理锁事故时少掉一半头发。