
做了几年微服务处理过抢购、订单幂等、定时任务调度这些场景之后你会发现一个绕不开的话题分布式锁。面试的时候它是高频题聊起来大家都能说上几句 Redis SETNX、ZooKeeper 临时节点但真正到线上锁失效、锁误删、锁续约、锁粒度这些问题一个比一个隐蔽稍不留神就会踩进坑里。这篇文章我想按“入门到精通”的路线把分布式锁这件事讲透。从它到底解决什么问题开始到主流实现方案的选型对比再手把手写一个生产可用的 Redis 分布式锁最后把我在线上遇到的坑和排查思路一并整理出来。适合正在做分布式系统开发的同学也适合准备面试、想系统梳理这块知识的人。不管你现在用的是 Java、Go 还是其他语言核心思路都是通用的。1. 分布式锁要解决的三大核心难题1.1 锁的本质与本地锁为什么在分布式环境下失效先搞清楚一个基础问题锁到底在锁什么本质上锁是一个互斥约定。同一时刻只允许一个执行体进入临界区操作共享资源。在单机时代这个约定由 JVM 的 synchronized、ReentrantLock或者操作系统的 mutex 来实现。它们的工作范围被限定在单个进程内靠的是内存中的状态标记。但系统一旦拆成多个服务、多个实例部署问题就来了。同一个订单的支付接口同时部署在 3 台机器上三份 JVM 各自持有一把本地锁它们互相之间根本不知道对方的存在。用户手速快一点同一个请求打到不同实例三份代码能同时通过本地锁的校验然后同时去扣库存、同时去改订单状态。这个场景其实特别直白本地锁的作用域是进程而分布式环境需要的是一个跨进程、跨机器的互斥约定。所以我们需要的“分布式锁”就是在多个进程都能访问到的某个公共存储系统上立一个标记谁拿到标记谁就有资格进入临界区其他人必须等待或者放弃。这个公共存储系统可以是一张数据库表、一个 Redis key、一个 ZooKeeper 节点也可以是一个 etcd 租约。它们充当的角色相当于多个人抢一个共享白板谁在白板上写下自己的名字谁就获得了操作权。1.2 分布式场景下的三大挑战互斥、超时、归属把锁从单机搬到分布式最难的不是“实现一个标记”而是同时满足下面三个约束。第一个是互斥性。同一时刻只能有一个客户端持有锁。这个听起来是废话但在实现层面并不容易。比如用 Redis 的 setnx 加锁如果忘了设置过期时间一个客户端加锁后进程宕机锁就永远不释放其他客户端永远拿不到锁这就破坏了互斥的“可用”前提。又比如用数据库悲观锁如果事务一直不提交行锁会一直占住还会拖垮连接池。第二个是超时机制。分布式环境下持有锁的客户端随时可能宕机、网络闪断、Full GC 停顿。锁必须有自动失效的能力否则一次异常就会导致整个系统卡死。这就是为什么任何分布式锁方案都要引入“过期时间”或者“租约”的概念。Redis 里是 PX 过期毫秒数ZooKeeper 里是 session 超时时间etcd 里是 lease TTL。但超时机制的引入又带来了新的问题锁持有者还没干完活锁就到期了另一个客户端拿到锁进来互斥被破坏。这是分布式锁最核心的矛盾后面讲续约机制的时候我们细说。第三个是归属校验。锁被创建之后可能在释放之前就被过期自动删掉了或者被某个延迟任务清理了。这时候一个客户端持有的锁物理上可能已经不属于它了。释放锁的时候如果只是简单地执行一个 del 命令就可能在锁已经被别人拿到的情况下把人家的锁给删掉。正确的做法是每个锁带上持有者的唯一标识释放前先校验、再删除整个过程要用原子操作保证。这三个约束是所有分布式锁方案的共同底层逻辑。看懂它们你就掌握了判断一个实现方案好坏的标准。1.3 可用性与一致性之间的取舍没有完美的锁在分布式领域CAP 理论是绕不开的。分布式锁本质上也是在一致性和可用性之间做取舍。当你用 Redis 做分布式锁你选择的是高性能、高可用但它在极端故障下可能丢失锁当你用 ZooKeeper 做分布式锁你选择了强一致但性能和运维复杂度都要付出代价。这不是说哪个方案绝对好而是要看业务场景。支付、库存这类强一致的业务可能更倾向于 ZooKeeper 或者 etcd而缓存更新、幂等防重、秒杀流量控制这类允许极小概率失误的业务Redis 是绝对的主流。我的建议是不要试图寻找一个“完美”的分布式锁而是要想清楚你的业务能容忍什么级别的失败再反推技术选型。后面我会用一个完整的对比表格帮你理清思路。2. 主流实现方案横评数据库、Redis、ZooKeeper 到底怎么选2.1 数据库实现悲观锁与乐观锁的真实表现我最早接触分布式锁就是用数据库实现的这也是很多老系统的常见做法。方案很简单建一张锁表里面放锁名称、持有者、过期时间这些字段。获取锁的时候就插入一条记录释放的时候就删除靠数据库的唯一索引保证锁名不重复从而保证互斥。更常见的做法是直接用悲观锁SELECT ... FOR UPDATE锁住一行记录事务结束自动释放实现最简单连 delete 都省了。但实际用起来问题不少。最典型的问题是长事务占用连接锁的持有时间等于事务时间如果业务里调了外部接口、做了耗时的 RPC数据库连接会被长时间占住数据库连接池一满整个服务就“假死”了。我当时就踩过这个坑一个批量任务在事务里处理几千条数据每条都调外部接口结果数据库连接被占满其他正常请求全部排队。乐观锁是另一种思路用版本号或者时间戳做 CAS 更新。但它本质上解决的是“数据更新冲突”不是“临界区互斥”。它适用于更新同一行数据的场景不适合锁住一段代码逻辑。而且乐观锁在高并发下会频繁重试对数据库的压力也很大。数据库锁的优势在于不引入额外组件、强一致、天然支持事务回滚。但它致命的弱点是性能瓶颈和连接资源开销。我现在的建议是只有在并发量很低、对锁的可靠性要求不高的内部管理系统中才考虑用数据库实现高并发核心链路不建议碰。2.2 Redis 实现性能之王的优势与它的软肋Redis 分布式锁是目前应用最广的方案。它的核心命令是SET key value NX PX timeoutNX 保证只有 key 不存在时才能设置成功PX 设置过期时间。这个命令是原子性的一行搞定性能和实现复杂度都极其友好。Redis 锁的优势很明显毫秒级延迟、命令简单、Redis 本身高可用方案成熟。所以大部分互联网公司只要用了 Redis分布式锁默认就选它。但 Redis 锁的软肋也很致命主从异步复制可能导致锁丢失。比如客户端 A 在主节点上加锁成功主节点还没把数据同步到从节点就宕机了哨兵把从节点提升为新主节点此时新主节点上没有这把锁。客户端 B 上来就能加锁成功互斥被破坏。Redis 官方也意识到这个问题所以作者提出了 Redlock 算法用多节点加锁来降低单点故障的影响但这个算法的安全性在分布式系统领域一直有激烈争论。关于 Redlock 到底靠不靠谱我在第三章专门讲。在绝大多数业务场景下我的判断是Redis 分布式锁的“极端情况下可能丢失锁”这个缺陷业务上是可接受的。因为 Redis 锁的常见使用场景是防重、限流、缓存击穿保护这些场景下即使出现极小概率的锁丢失最坏结果也就是重复执行一次操作而幂等性设计通常能兜住。真正严格到一分一毫都不能错的场景一开始就不该用 Redis。2.3 ZooKeeper 与 etcd一致性优先的另一种解法ZooKeeper 实现分布式锁利用的是它的一致性模型和临时顺序节点。大致流程客户端在锁目录下创建一个临时顺序节点然后检查自己是不是序号最小的那个是就获得锁不是就监听序号比它小的节点的删除事件等前一个节点释放后再尝试获取。这个机制的好处是客户端崩溃后Session 结束临时节点自动消失锁自动释放不需要额外的超时机制来兜底。这是 ZK 锁比 Redis 锁更“安全”的根本原因。但 ZK 锁的问题在于性能和复杂性。每次加锁解锁都要经历节点创建、删除、事件监听ZooKeeper 集群的写性能和节点规模都不如 Redis。客户端数量多时还可能产生“羊群效应”大量客户端同时被唤醒去竞争锁。虽然 Curator 框架提供的 InterProcessMutex 已经处理了大部分细节开发效率高了不少但 ZooKeeper 集群本身的运维成本是实打实的。etcd 的实现方式和 ZK 类似用的是租约Lease机制。客户端创建一个带 TTL 的租约然后把锁 key 绑定到租约上获取锁时通过事务保证原子性。etcd 的线性一致性读比 ZK 更友好API 也更现代化。如果你的公司基础设施里已经有 etcd用它做分布式锁是不错的选择如果没有为了做分布式锁单独引入一套 etcd 或 ZK性价比就不高了。2.4 横向对比清单场景、成本、运维、可靠性一目了然为了让你选型的时候更直观我把三个方案放在一张表里对比。维度数据库锁Redis 锁ZooKeeper / etcd 锁互斥可靠性高事务隔离保证高但主从切换可能丢失很高一致性协议保证自动释放机制事务结束/连接断开过期时间需续约临时节点/租约自动过期性能表现差数据库是瓶颈极好毫秒级一般ZK 写性能偏弱实现复杂度低低较高运维成本无额外组件依赖 Redis 高可用需维护独立集群典型场景内部系统、低频任务高并发防重、秒杀、缓存保护强一致场景、分布式协调看完表格你应该能得出自己的结论了。绝大多数互联网场景Redis 锁是综合性价比最高的对一致性极其敏感、且团队有能力运维 ZK/etcd 的场景选择后者数据库锁只适合老系统维护或者并发极低的场景。不要被“谁更高级”绑架适合你的业务和团队的就是最好的。3. Redis 分布式锁从最基础实现到生产级代码3.1 第一版SETNX 加过期时间的“入门写法”既然 Redis 是主流我具体展开它的实现方式一步步从入门写法演进到生产级。先看最基础的版本这也是很多初学者写出来的代码# 获取锁键为订单号值随意过期时间 30 秒 SET order:pay:10086 1 NX PX 30000这个写法解决了互斥和自动过期两个问题。key 存在就返回失败key 不存在就设置成功并自动过期。但它在生产环境有两个致命缺陷。第一个缺陷是锁没有归属标识。假设客户端 A 加锁成功业务执行超过了 30 秒锁自动过期了。此时客户端 B 加锁成功开始执行。然后 A 业务终于执行完执行 del 释放锁结果把 B 持有的锁删掉了。B 还在跑业务但锁已经没了其他客户端 C 可以再次加锁成功三个客户端同时进入临界区。要解决这个问题value 必须是一个唯一标识释放锁的时候先比较是不是自己的再删除。第二个缺陷是极端情况下加锁命令本身没问题但业务代码可能在加锁后、设置过期时间前发生异常。为什么最早的写法是 setnx 和 expire 两条命令分开写因为早期 Redis 版本没有 NX PX 合一的命令两步操作中间一旦宕机锁就永远不会过期。好在现在SET key value NX PX本身是原子的这个坑在语法层面已经回避了但理解历史背景能帮助你理解为什么现在都推荐这个命令。3.2 生产级第二版唯一标识与 Lua 脚本释放针对上面两个缺陷生产级的 Redis 锁至少要做到两点加锁时生成一个唯一的 value释放锁时用 Lua 脚本先校验、再删除。先看加锁// 生成全局唯一的锁标识UUID 就可以 String lockKey order:pay: orderId; String lockValue UUID.randomUUID().toString(); // 加锁过期时间建议放到配置里方便调整 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { // 拿到锁执行业务 }再看释放锁的 Lua 脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endLua 脚本在 Redis 服务端原子执行所以“判断 value 是否相等 删除 key”这两个操作不会被其他客户端命令穿插。如果你在 Java 里用 Jedis 或者 Redisson其实内置了这段逻辑但最好自己理解脚本面试也常考这里。这里有个容易忽略的细节value 的比较必须用字符串相等比较不能用因为 Lua 里 Redis 返回值是字符串不同语言客户端封装后也要注意类型转换。我在 Go 的项目里就见过有人写成了字节数组直接比较结果一直不相等锁永远释放不掉最后只能用del硬删反而引入了误删风险。还有一点拿到锁之后一定要在 finally 块里释放锁。如果业务抛异常锁也要释放否则其他线程就只能等锁过期。但这个“释放”必须发生在业务真正执行完之后所以 finally 里也要判断当前锁是否仍然属于自己避免把别人的锁删掉。这一步综合起来就是为什么释放锁一定要用 Lua 脚本而不是先 get 再 del 的原因。3.3 进阶看门狗续约机制与可重入设计上一版解决了不少问题但还有一个大麻烦业务执行时间超过锁的过期时间怎么办最简单的办法是把过期时间设长一点比如 30 秒、60 秒。但设长也会带来问题客户端崩溃了锁最长要等 60 秒才能自动释放这段时间其他客户端都在阻塞等待系统吞吐量直线下降。生产级方案是引入“续约机制”业界一般叫“看门狗”。思路是加锁成功后开一个后台线程每隔一段时间检查一次业务是否还在执行如果还在执行就自动把锁的过期时间延长。业务执行完释放锁的同时关掉后台线程。这样锁的有效期不再是一个固定值而是跟着业务执行时长动态伸缩。Redisson 里的 WatchDog 就是这个思路。它的默认实现是加锁后锁的默认过期时间是 30 秒后台一个定时任务每 10 秒检查一次如果锁还在持有就把过期时间重置为 30 秒。用 Redisson 的代码非常简单RLock lock redissonClient.getLock(order:pay: orderId); try { // 内部自动开启看门狗续约 boolean locked lock.tryLock(5, TimeUnit.SECONDS); if (!locked) { // 拿不到锁的处理 return; } // 执行真正的业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }注意Redisson 的看门狗只在没有显式指定 leaseTime 时才生效。如果你调用了tryLock(waitTime, leaseTime, unit)并传了 leaseTime锁到期后就直接释放不会续约。这个细节很坑我见过好几个人传了 leaseTime 后业务还在跑锁莫名被释放线上出现并发异常一看代码才发现是自己手写了 leaseTime 把看门狗给覆盖了。再说可重入。同一个线程在已经持有锁的情况下再次调用加锁方法如果锁不支持重入就会死锁。Redisson 的默认锁是支持重入的它通过 Redis 的 Hash 结构和 ThreadLocal 记录重入次数。但我的建议是业务代码尽量避免依赖可重入。分布式锁的作用是保护一段短小的临界区如果出现锁嵌套多半设计上也出了问题。真遇到递归逻辑需要重入优先考虑能不能把递归改成循环或者把内层逻辑提到锁外面。可重入会显著增加锁的实现复杂度和排查难度。3.4 关于 Redlock它到底解决了什么为什么争议不断说到 Redis 分布式锁一定会聊到 Redlock。这是 Redis 作者 Antirez 提出的多节点锁算法客户端同时向 N 个独立的 Redis 节点请求加锁只要超过一半N/2 1节点加锁成功并且总耗时小于锁的过期时间就视为加锁成功。它想解决的是单点故障问题比如某个 Redis 节点宕机导致锁丢失。但分布式系统领域的几位大牛比如 Martin Kleppmann公开反驳过 Redlock核心论点是它在异步网络模型下仍然不能保证绝对安全。问题不在算法本身而在于它依赖的“时钟”和“网络延迟”并不是可靠的。比如客户端 A 在多数节点上加锁成功但在返回给业务前发生了 GC 停顿停顿期间锁过期了客户端 B 拿到锁A 恢复后以为自己还持有锁两个客户端同时进入临界区。Redlock 本质上只是降低了锁丢失的概率并没有消除锁丢失的可能性。我的看法是Redlock 的实际价值有限。如果你的业务要求很强的一致性应该直接选 ZooKeeper 或 etcd它们从一致性协议层面保证了线性一致如果你能容忍 Redis 场景下的极小概率丢失单 Redis 实例加上主从高可用已经够用。Redlock 增加了几个节点的部署成本和实现复杂度却没有带来质的可靠性提升性价比并不高。所以我建议架构选型阶段直接跳过 Redlock不要为了“听起来很专业”而引入它。3.5 一套可以直接抄的简易 Java 实现骨架最后给你一个不算复杂但可用的 Java 实现骨架。这个版本支持唯一标识防误删、Lua 原子释放、看门狗续约简化版适合理解原理后自己维护。生产环境我依然建议优先用 Redisson但这个骨架的价值在于能让你真正理解每一行代码在干什么。public class SimpleRedisLock { private final StringRedisTemplate redisTemplate; private final String lockKey; private final String lockValue UUID.randomUUID().toString(); private final long expireMillis; private volatile boolean locked; private volatile ScheduledFuture? watchdogFuture; // 加锁等待时间 waitMillis超过时间拿不到就返回 false public boolean tryLock(long waitMillis) throws InterruptedException { long deadline System.currentTimeMillis() waitMillis; while (System.currentTimeMillis() deadline) { Boolean success redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofMillis(expireMillis)); if (Boolean.TRUE.equals(success)) { locked true; startWatchdog(); return true; } Thread.sleep(50); } return false; } private void startWatchdog() { // 每隔 expireMillis / 3 时间续约一次 watchdogFuture scheduledExecutor.scheduleAtFixedRate(() - { if (!locked) { return; } String current redisTemplate.opsForValue().get(lockKey); if (lockValue.equals(current)) { redisTemplate.expire(lockKey, Duration.ofMillis(expireMillis)); } }, expireMillis / 3, expireMillis / 3, TimeUnit.MILLISECONDS); } public void unlock() { locked false; if (watchdogFuture ! null) { watchdogFuture.cancel(true); } String releaseScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute( new DefaultRedisScript(releaseScript, Long.class), Collections.singletonList(lockKey), lockValue ); } }这段代码有三个地方值得注意。第一定时续约的间隔用了过期时间的三分之一不要让续约过于频繁也不要让锁在两次续约之间过期。第二解锁时一定要先取消看门狗再删除锁顺序乱了会出现锁删了但后台线程还在续约又把一个不存在的 key 建出来的问题。第三locked字段要加 volatile它在不同线程间可见。这些细节就是生产环境和 demo 的差距。4. 生产环境实战经验踩坑记录与问题排查4.1 五个最容易翻车的细节每一条都是线上事故先说第一个锁与事务的生命周期冲突。这是我在真实项目里遇到最多的问题。Spring 的Transactional事务默认在方法返回后、由代理类统一提交。如果你在方法内部先释放了锁再执行方法返回事务提交发生在锁释放之后。这意味着其他客户端可能在事务还没提交时就获得锁并读取数据读到的是旧数据或者中间状态。解决办法是把加锁逻辑放到事务外或者让锁的释放晚于事务提交。可以拆成两层方法外层加锁、内层走事务也可以用编程式事务在事务提交之后再释放锁。第二个锁的粒度设计。很多人习惯用一个全局锁“锁住所有订单”这在高并发下等于把所有请求串行化性能极差。正确做法是锁的粒度要贴合业务对象比如订单锁用order:{orderId}库存锁用stock:{skuId}。key 的设计直接影响并发度这也是分布式锁最容易忽略的性能问题。第三个获取锁失败后的处理方式。是直接返回失败、抛异常还是自旋等待没有统一答案取决于业务。接口幂等场景拿不到锁就返回“重复请求”秒杀场景拿不到锁就快速返回“已售罄”定时任务场景可以自旋等待几秒。我的经验是绝大多数场景不要用无限制自旋给等待时间设一个上限比如 3 秒或 5 秒避免锁长时间不释放时请求全部阻塞堆积。第四个锁的监控与告警。锁加上了就万事大吉不是的。线上出问题的时候排查锁是否正常释放、是否有人长时间持锁、解锁是否报错都需要日志和监控。建议每次加锁、解锁都打点记录 key、耗时、结果定时任务里可以统计锁等待时长和持有时长。Redis 侧也要关注锁相关 key 的数量变化突然大量堆积说明有锁没释放。第五个代码层面锁的重试策略。Redis 命令本身很快但业务方法可能很长。我见过很多团队的锁实现里没有重试机制一旦拿不到锁就直接抛异常导致用户看到大量失败请求。合理的做法是在应用层面做少量重试并带上 sleep 间隔间隔建议用随机值避免大量请求同时重试形成惊群。4.2 用注解和 AOP 封装从“锁散落各处”到“一键加锁”分布式锁用多了你会发现如果每次都在业务代码里手写 tryLock/unlock代码会非常臃肿而且很容易漏掉 finally 释放。我的做法是做一个自定义注解加 AOP 切面把加锁解锁逻辑统一收敛起来。注解设计很简单Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DistributedLock { String key(); // 锁 key支持 SpEL 表达式比如 #orderId long waitMillis() default 3000; // 等待锁的时间 long expireMillis() default 30000; // 锁过期时间 }切面的核心逻辑解析 SpEL 生成真正的锁 key调 tryLock拿到锁就执行目标方法最终在 finally 里释放锁。如果拿不到锁可以根据注解上的策略抛出业务异常或者返回兜底结果。用 AOP 之后业务代码只需要一个注解锁的细节全部隔离团队的代码风格也统一了。但 AOP 封装也有坑。最大的坑是自调用问题。同一个类内部方法 A 调用方法 BB 上有DistributedLock注解此时 Spring 代理不生效注解直接被跳过锁根本不会加。解决方案是不要自调用或者把 B 方法拆分到另一个 Bean 里。这个坑我提醒过很多同事属于“代码看着没问题压测才暴露”的典型问题。4.3 压测与监控锁到底稳不稳要有数据说话分布式锁上线前最好做一轮针对性压测。压测口径不要只盯着接口的 QPS要看这几个指标请求成功率、锁获取失败率、锁等待耗时分布、业务执行耗时、Redis 侧 CPU 和慢查询。只有这些数据全绿你才能说锁是稳的。我做过一次印象很深的压测。一个秒杀接口用分布式锁保护库存扣减第一次压测 500 并发结果接口成功率只有 92%不是业务问题而是大量请求超时。查日志发现等待时间设的是 5 秒而 Redis 在并发 2000 的时候出现了几次慢查询锁获取本身的耗时被拉长到几十毫秒。后来把 Redis 连接池调大、把锁等待时间缩短、加了随机重试成功率才恢复到 99.9% 以上。这个案例说明分布式锁的瓶颈往往不在锁算法本身而在底层的 Redis 连接、网络和线程池。监控方面我习惯在加锁和解锁的关键路径加埋点。加了锁之后业务执行超过阈值就告警解锁失败的异常必须记录下来。这样线上一旦出现锁异常可以快速定位是业务耗时长、锁被过期清理、还是释放脚本有 bug。不要等用户投诉了再去翻日志分布式系统的排查成本很高前置监控比事后救火重要得多。4.4 高频问题速查表遇到问题照着排查最后整理一份我在工作中经常遇到的分布式锁问题速查表你可以收藏起来线上出问题了拿它当排查手册。问题现象可能原因排查与解决思路锁获取一直失败请求大量超时锁未释放 / 未设置过期时间 / 业务卡死查 Redis key 是否存在和 TTL看业务日志里有无未进入 finally 的返回路径锁自动释放后并发进入临界区业务执行时间超过过期时间且无续约机制引入看门狗续约或者根据业务耗时设置足够长的过期时间删除了别人的锁value 不是唯一标识或者释放时没有校验使用 UUID 作为 value释放用 Lua 脚本先比较再删除锁不生效多个实例同时进入自调用绕过 AOP或者不同服务用了不同的 Redis 库检查注解是否在代理方法上确认两个服务连的 Redis 是同一个库Redis 重启后锁丢失主从切换未同步的锁 key 丢失评估业务容忍度必要时使用多节点方案或换 ZK/etcd加了锁之后吞吐量骤降锁粒度太粗比如所有订单共用一把锁把锁 key 细化到业务对象维度比如订单号、SKU 维度解锁时报错 Unknown keyskey 过期自动消失后执行 Lua 的 KEYS 不存在判断返回值锁已自动释放时解锁可以不做处理但要记录日志排查这些问题的时候有一个通用的出发点先确认锁当前在不在再确认锁是谁的最后确认锁该不该在。分布式锁的问题九成都能用这三步理清。第一步查exists和ttl第二步查 value 对应的持有者第三步结合业务日志判断是否正常释放。我个人在实际项目里还有一个习惯所有分布式锁的 key 都统一加一个业务前缀比如lock:order:并且强制要求锁 value 必须是 UUID。因为有了前缀排查时可以用SCAN命令扫出所有锁 key有了 UUID任何一环出了问题你都能明确知道这把锁曾经属于谁。我见过太多团队锁 value 写死一个常量出了并发问题根本不知道是哪个实例加的锁只能靠猜。这个细节用不上几次但每次用上都值回票价。