
分布式锁最直接的作用就是解决在分布式系统里多个服务实例同时操作同一份共享资源时可能引发的数据不一致问题。比如一个商品库存为1两个订单请求同时到达不同的服务器如果没有锁两个请求都可能判断库存足够并完成扣减最终导致超卖。分布式锁就是为了在分布式环境下实现类似单机多线程编程中synchronized或ReentrantLock的效果确保在任意时刻只有一个客户端能持有锁并执行业务逻辑。如果你正在开发微服务、处理高并发订单、设计任务调度系统或者准备面试理解分布式锁的实现和坑点是绕不开的。很多人一上来就研究 Redis 的setnx命令但实际落地时锁的续期、释放的原子性、集群环境下的可靠性才是真正决定方案能否稳定运行的关键。这篇文章不会只讲概念我会结合常见的实现方式拆解从选型、实现到生产环境避坑的完整链条。重点不是记住几种实现的名字而是搞清楚在什么场景下该用哪种方案以及每种方案在真正部署时需要额外关注哪些配置和监控点。1. 先明确核心诉求我们需要什么样的锁在动手实现或选型之前得先想清楚一个能在生产环境用的分布式锁到底需要满足哪些条件。这直接决定了后续的技术方案选择。1.1 必须满足的五个基本特性这五个特性是分布式锁的“及格线”缺一不可互斥性这是最基本的要求在任意时刻只能有一个客户端持有锁。安全性锁只能由持有它的客户端释放其他客户端不能释放别人的锁。防止误删。不死锁死锁预防即使持有锁的客户端崩溃或发生网络分区锁最终也能被释放避免系统永久阻塞。这通常通过给锁设置一个过期时间来实现。容错性提供锁服务的存储节点如 Redis部分宕机时锁机制仍然能正常工作或者能快速、安全地失效而不是返回一个错误的状态。这指向了集群方案。高性能与高可用加锁、解锁的操作要足够快不能成为系统瓶颈。同时锁服务本身要具备高可用性。1.2 容易被忽略的两个进阶要求除了上述基本特性在复杂的生产环境中还有两个要求会直接影响系统的健壮性可重入性同一个线程或客户端在已经持有锁的情况下可以再次成功获取该锁。这在方法递归调用或同一个客户端需要多次进入临界区的场景下非常重要。不支持可重入的锁容易让自己把自己锁死。公平性是否按照客户端申请锁的顺序来授予锁。在极端高并发争抢下公平锁可以避免某些客户端长时间饥饿。但实现公平锁通常需要更复杂的机制如队列会牺牲一部分性能。明确了这些要求我们再看具体实现方案时就有了清晰的评估标准这个方案在满足基本特性的前提下对可重入、公平性、性能、可用性的支持程度如何2. 三种主流实现方式的原理与落地细节网络热词里提到了“三种实现方式”通常指的是基于数据库、Redis 和 Zookeeper/Etcd 的方案。我们逐一拆解重点放在“怎么实现”和“会出什么问题”上。2.1 基于数据库的实现最简单但局限也最大这是最直观的一种方式利用数据库的唯一约束或行锁来实现互斥。实现方式一唯一索引表创建一张锁表比如叫distributed_lock其中有一个唯一字段lock_key表示资源标识。加锁时执行 INSERT 语句尝试插入这条记录。由于lock_key的唯一约束只有第一个插入成功的客户端算加锁成功。解锁时删除这条记录。-- 加锁尝试 INSERT INTO distributed_lock (lock_key, client_id, expire_time) VALUES (order_stock_123, client_A, 2023-10-27 12:00:00); -- 解锁 DELETE FROM distributed_lock WHERE lock_key order_stock_123 AND client_id client_A;实现方式二乐观锁基于数据版本号或条件更新。在业务数据表如库存表增加一个版本号字段version。更新时必须带上查询时取得的版本号。-- 1. 查询当前数据和版本 SELECT stock, version FROM product WHERE id 1; -- 假设查得 stock10, version5 -- 2. 更新时校验版本 UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 5; -- 如果返回影响行数为1则成功持有“锁”并更新为0则说明版本已变获取“锁”失败。实现方式三悲观锁SELECT ... FOR UPDATE利用数据库的行级排他锁。BEGIN; SELECT * FROM distributed_lock WHERE lock_key order_stock_123 FOR UPDATE; -- ... 执行业务逻辑 ... COMMIT;落地评估与坑点性能瓶颈数据库的 IO 和连接数在高并发下容易成为瓶颈不适合超高并发场景。死锁与过期对于唯一索引表如果客户端崩溃锁记录无法自动删除需要引入独立的过期时间字段和定时清理任务增加了复杂度。可用性依赖锁的可用性完全依赖于数据库。数据库主从切换时可能因数据延迟导致锁状态不一致从库读到旧的未加锁状态。建议场景并发压力不大、已有数据库依赖、且不希望引入额外中间件的系统。更常用于“防重提交”、“幂等性校验”等场景而非严格的秒杀锁。注意不要轻易使用SELECT ... FOR UPDATE来做分布式锁它锁住的是数据库连接会话如果业务逻辑执行时间过长会导致数据库连接长时间被占用极易引发连接池耗尽。2.2 基于 Redis 的实现最流行但细节决定成败Redis 凭借其高性能和丰富的原子命令成为分布式锁的首选。核心命令是SET key value NX PX timeout。NX仅当 key 不存在时才设置实现互斥。PX设置 key 的过期时间毫秒防止死锁。value必须设置为一个唯一标识如 UUID 线程ID用于安全释放锁。基础加锁与解锁# 加锁 SET lock:order_stock_123 unique_client_id_value NX PX 30000 # 解锁使用Lua脚本保证原子性 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end解锁必须使用 Lua 脚本因为“判断锁归属”和“删除锁”必须是原子操作否则可能释放别人的锁。落地核心问题与 Redisson 解决方案基础实现有两大核心难题锁续期WatchDog业务逻辑执行时间可能超过锁的过期时间。如果锁自动过期而业务还在执行其他客户端就能获取到锁导致临界区被同时进入。需要一种机制在业务未完成时自动延长锁的持有时间。高可用问题在 Redis 单点或普通主从架构下如果主节点宕机且锁数据未同步到从节点从节点升级为主后新的客户端可能再次获取到同一个锁导致互斥失效。这就是为什么 Redisson 客户端如此流行。它封装了完整的解决方案可重入锁内置实现同一个线程可重复加锁。看门狗机制后台线程定期检查并续期锁默认超时时间是 30 秒看门狗每 10 秒续期一次。丰富的锁类型公平锁、联锁、红锁等。简化 API用法类似 Java 的Lock接口对开发者友好。关于 RedLock红锁的争议RedLock 是 Redis 作者提出的一种算法用于在多个独立的 Redis 主节点上同时申请锁当从超过半数的节点获取成功时才算真正持有锁。旨在解决单点/主从切换时的锁失效问题。 然而该算法存在争议如 Martin Kleppmann 曾发文质疑其实现复杂且对网络延迟和系统时钟非常敏感。在实践中多数场景下使用 Redis 哨兵或集群模式并配合合理的过期时间其可靠性已经足够。只有在极端要求金融级一致性的场景下才需要考虑 RedLock 或更可靠的方案如 Zookeeper并充分测试。配置建议锁过期时间设置一个远大于业务平均执行时间的值如 30s为网络抖动和 Full GC 留出余量。使用连接池避免每次加解锁都创建新连接。监控监控 Redis 节点的内存、网络和锁 key 的数量。2.3 基于 Zookeeper/Etcd 的实现最可靠但性能有折衷这类协调服务通过其临时顺序节点和Watch 机制提供了原生的分布式锁抽象。Zookeeper 实现原理加锁客户端在指定的锁节点如/locks/stock_123下创建临时顺序节点。排队客户端获取/locks/stock_123下所有子节点并按序号排序。判断如果自己创建的节点是序号最小的则获取锁成功。等待如果不是最小的则向比自己序号小的前一个节点注册Watcher监听。锁释放与通知当前一个节点被删除锁被释放时Zookeeper 会通知当前客户端它再次尝试判断自己是否变为最小节点。解锁/会话结束客户端主动删除自己创建的节点或者会话超时断开时临时节点会被自动删除锁随之释放。优势分析天然解决死锁临时节点在客户端会话结束时自动清除相当于内置了“过期”机制。安全性高节点只能由创建它的客户端删除。可实现公平锁顺序节点天然形成了排队队列。集群强一致Zookeeper 的 ZAB 协议保证了集群各节点状态的强一致性不存在 Redis 主从异步复制导致的数据丢失问题。劣势与坑点性能较低相比 Redis写操作创建节点性能较低因为需要达成集群共识。“羊群效应”如果有很多客户端在等待同一个锁当锁释放时所有等待的客户端都会被通知并同时尝试获取锁对 Zookeeper 服务器造成压力。优化方法是只让后一个节点监听前一个节点。依赖会话锁的持有与客户端和 Zookeeper 服务器的会话绑定。如果网络闪断导致会话超时锁会意外释放而业务可能还在执行。需要处理好这种异常情况。运维复杂度需要维护 Zookeeper 集群。建议场景对锁的可靠性要求极高可以接受一定性能损耗的场景如分布式选主、配置管理、以及一些核心的金融交易流程。3. 从“能用”到“用好”生产环境的关键实践选定了方案写好了加锁解锁代码只是第一步。要让分布式锁在生产环境稳定运行还需要处理好以下几个关键环节。3.1 锁的粒度与命名设计锁的粒度越细系统并发度越高但管理也越复杂。细粒度锁lock:order:${orderId}。适用于对单个订单、单个用户的操作。并发度高但 key 数量可能巨大。粗粒度锁lock:order:stock。适用于对整个库存表的操作。管理简单但并发度极低成为性能瓶颈。命名规范建议使用清晰的业务前缀如业务:资源类型:资源标识。例如trade:order_pay:123456。这便于后续的监控和排查。3.2 避免锁失效带来的业务风险这是分布式锁最危险的问题之一锁在业务执行期间意外失效。原因1业务执行时间 锁过期时间。对策合理评估并设置足够长的过期时间。使用像 Redisson 的看门狗机制进行自动续期。原因2网络问题导致锁服务误判客户端存活如 Zookeeper 会话超时。对策业务代码需要具备幂等性和状态校验。即使锁意外失效导致另一客户端进入在执行业务前应再次检查资源状态如库存是否还大于0。这是最后的防线。3.3 加锁与解锁的标准范式一个健壮的加锁代码块应该遵循以下模式// 伪代码以Redisson为例 RLock lock redissonClient.getLock(myLock); try { // 尝试加锁最多等待100秒上锁以后30秒自动解锁 boolean isLocked lock.tryLock(100, 30, TimeUnit.SECONDS); if (isLocked) { // 成功获取锁执行业务临界区代码 doBusiness(); } else { // 获取锁失败进行相应处理如快速失败、排队、记录日志等 handleLockFailed(); } } catch (InterruptedException e) { // 处理中断异常 Thread.currentThread().interrupt(); // 恢复中断状态 handleInterruption(); } finally { // 必须在finally块中检查并释放锁 if (lock.isHeldByCurrentThread()) { // 关键检查是否还持有锁 lock.unlock(); } }要点使用tryLock并设置等待时间避免线程无限期阻塞。在finally块中解锁确保锁一定被释放。解锁前检查锁持有者防止释放其他线程的锁Redisson 内部已处理自定义实现需注意。3.4 非阻塞锁与异步处理在高并发场景下如果获取锁失败不应让线程空等或阻塞。快速失败获取锁失败后立即向客户端返回“系统繁忙”等提示。异步重试将获取锁的请求放入消息队列由后台 worker 异步处理处理完成后通知前端。排队队列在业务层或使用专门的队列服务如 Kafka、RocketMQ进行请求排队缓解对锁服务的瞬时压力。4. 问题排查当锁不工作时从哪里看起线上系统发现数据不一致怀疑是分布式锁问题可以按照以下顺序排查。4.1 检查锁是否真的生效查看锁服务数据Redis使用GET lock_key查看锁是否存在TTL lock_key查看剩余生存时间。观察 value 是否与预期的客户端标识匹配。Zookeeper使用zkCli.sh查看锁节点路径下的临时顺序子节点列表和状态。业务日志在加锁和解锁的关键位置打印详细的日志包括锁 key、客户端标识、加锁/解锁时间、是否成功等。这是最直接的证据。链路追踪如果接入了 APM如 SkyWalking, Zipkin查看一次请求的调用链分析锁获取和业务执行的耗时判断是否存在业务超时导致锁过期。4.2 分析锁失效或误释放的原因场景日志显示 A 客户端持有锁但最终是 B 客户端操作了资源。可能1RedisA 客户端的业务逻辑执行时间超过了锁的过期时间锁自动释放后B 客户端获得锁。检查业务耗时和锁超时时间设置。可能2RedisA 客户端在解锁时由于网络延迟或 GC 停顿解锁的 Lua 脚本执行时锁已过期且被 B 客户端获取导致 A 删除了 B 的锁。确保解锁脚本的原子性并使用唯一客户端标识。可能3ZookeeperA 客户端与 Zookeeper 服务器网络闪断会话超时临时节点被删除。检查 Zookeeper 会话超时设置和客户端网络状况。场景多个客户端似乎同时获得了锁。可能Redis主从客户端在主节点加锁成功但锁数据还未同步到从节点主节点宕机从节点升级为新主且不包含该锁信息导致其他客户端可以再次加锁。考虑使用 RedLock 或切换到 Redis 集群模式并评估业务是否能接受极小概率的此类事件。4.3 性能问题排查加锁慢检查锁服务Redis/ZK的监控看 CPU、内存、网络 IO 是否正常。检查客户端到锁服务的网络延迟。检查客户端是否使用了连接池。高并发下大量失败检查锁的粒度是否过粗。考虑引入排队机制或采用更细粒度的锁。4.4 准备一个排查清单当出现问题可以快速对照锁记录存在吗查 Redis/ZK锁的持有者是谁是预期的客户端吗查 value/节点数据锁还有多久过期查 TTL/节点状态持有锁的客户端还在运行吗查应用日志、进程状态业务逻辑执行了多久查业务日志时间戳或 APM 数据解锁操作成功了吗查解锁日志锁服务集群状态健康吗查 Redis/ZK 集群监控我个人更倾向于在项目初期就采用成熟的客户端如 Redisson并做好完备的日志和监控这比后期从零开始排查一个自研的、有缺陷的锁要高效和安全得多。分布式锁是一个典型的“看起来简单做起来坑多”的组件理解其原理和边界选择合适的方案并配以良好的工程实践才能真正让它为系统的稳定性保驾护航。