分布式锁实现:从 Redis 到 ZooKeeper 的选型与生产级方案

📅 2026/6/23 9:08:27
分布式锁实现:从 Redis 到 ZooKeeper 的选型与生产级方案
分布式锁实现从 Redis 到 ZooKeeper 的选型与生产级方案一、并发竞争与数据一致性分布式锁要解决的根本问题在分布式系统中多个服务实例可能同时访问共享资源——扣减库存、发放优惠券、配置变更推送等场景都需要互斥访问。单机环境下用synchronized或ReentrantLock即可解决但在跨进程、跨机器的分布式环境下这些本地锁机制完全失效。分布式锁的核心挑战在于网络不可靠、时钟不同步、进程随时可能崩溃。一个设计不当的分布式锁在正常情况下看似工作正常一旦出现网络分区或 GC 停顿就可能导致锁无法释放、死锁或多个客户端同时持有锁。这类问题在线上极难复现却可能在流量高峰时集中爆发造成超卖、数据错乱等严重后果。二、分布式锁底层机制三种实现方案的原理与对比分布式锁的主流实现方案有三种基于 Redis 的 RedLock、基于 ZooKeeper 的临时顺序节点、基于 etcd 的租约机制。下图展示了三种方案的核心机制flowchart LR subgraph Redis方案 R1[客户端A SETNX 获取锁] -- R2[设置过期时间 防死锁] R2 -- R3[执行业务逻辑] R3 -- R4[LUA脚本 原子释放锁] end subgraph ZK方案 Z1[客户端A 创建临时顺序节点] -- Z2[Watch前一个节点] Z2 -- Z3[前一个节点删除 获得锁] Z3 -- Z4[会话断开 节点自动删除] end subgraph etcd方案 E1[客户端A 申请租约] -- E2[Put key with lease] E2 -- E3[执行业务逻辑] E3 -- E4[撤销租约 释放锁] end style R1 fill:#f99,stroke:#333 style Z1 fill:#9f9,stroke:#333 style E1 fill:#99f,stroke:#3332.1 Redis 分布式锁SETNX 过期时间的原子性保障Redis 锁的核心是SET key value NX PX timeout命令NX 保证互斥PX 设置过期防止死锁。释放锁时必须用 Lua 脚本校验 value防止误删其他客户端的锁。Redis 锁的优势是性能极高单次加锁耗时在亚毫秒级劣势是在 Redis 主从切换时可能出现锁丢失。2.2 ZooKeeper 分布式锁临时顺序节点与 Watch 机制ZK 锁的实现基于临时顺序节点每个客户端在锁目录下创建一个临时顺序节点节点序号最小的获得锁其他客户端 Watch 自己前一个节点前一个节点删除时被唤醒。ZK 锁的优势是强一致性ZAB 协议保证客户端会话断开时临时节点自动删除不会出现锁无法释放的情况劣势是性能较低每次加锁需要多次网络交互。2.3 etcd 分布式锁租约机制与 Revision 排序etcd 锁基于租约Lease机制客户端创建租约将 Key 绑定到租约上租约到期自动删除。通过 Revision 排序实现公平锁。etcd 锁的优势是支持租约续期和 TTL且基于 Raft 协议保证强一致性。三、生产级分布式锁实现3.1 Redis 分布式锁Redisson 的看门狗机制/** * 基于 Redisson 的分布式锁封装——看门狗自动续期 * 为什么用 Redisson 而非自己写 Lua 脚本 * 因为锁续期看门狗的实现极其复杂 * 需要在业务执行期间定时续约业务完成后精准停止 * 还要处理续约失败、客户端崩溃等边界情况 */ Component public class RedisDistributedLock { private final RedissonClient redissonClient; /** * 加锁并执行业务逻辑 * 为什么默认不指定 leaseTime * 不指定 leaseTime 时 Redisson 会启动看门狗 * 每 10 秒lockWatchdogTimeout/3自动续期一次 * 避免业务未执行完锁就过期的问题 */ public T T executeWithLock(String lockKey, long waitTime, TimeUnit unit, SupplierT task) { RLock lock redissonClient.getLock(lockKey); boolean acquired false; try { // waitTime: 最大等待获取锁的时间 // leaseTime: 不传则启动看门狗自动续期 acquired lock.tryLock(waitTime, -1, unit); if (!acquired) { throw new DistributedLockException( 获取分布式锁失败: lockKey); } return task.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new DistributedLockException(获取锁被中断, e); } finally { if (acquired lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * 可重入锁嵌套场景——库存扣减 订单创建 * 为什么需要可重入因为业务方法之间可能存在调用关系 * 不可重入锁会导致同一线程死锁 */ public void deductStockWithReentrant(Long skuId, int count) { String stockLock stock:lock: skuId; executeWithLock(stockLock, 3, TimeUnit.SECONDS, () - { // 第一层锁库存扣减 int remain stockMapper.deduct(skuId, count); if (remain 0) { throw new ServiceException(库存不足); } // Redisson 可重入锁同一线程可再次获取同一把锁 String orderLock order:lock: skuId; executeWithLock(orderLock, 3, TimeUnit.SECONDS, () - { orderMapper.create(skuId, count); return null; }); return null; }); } }3.2 ZooKeeper 分布式锁Curator InterProcessMutex/** * 基于 Curator 的 ZK 分布式锁——强一致性场景首选 * 为什么在库存扣减场景选择 ZK 锁而非 Redis 锁 * 因为库存是资金相关数据强一致性优先于性能 * ZK 的 ZAB 协议保证锁状态在多数节点确认后才返回成功 */ Component public class ZkDistributedLock { private final InterProcessMutex mutex; public ZkDistributedLock(CuratorFramework zkClient, String lockPath) { this.mutex new InterProcessMutex(zkClient, lockPath); } public T T executeWithLock(long timeout, TimeUnit unit, SupplierT task) { try { // Curator 的 InterProcessMutex 底层实现 // 1. 创建临时顺序节点 /lock/lock-0000000001 // 2. 获取所有子节点并排序 // 3. 如果自己是最小序号获得锁 // 4. 否则 Watch 前一个节点等待通知 if (!mutex.acquire(timeout, unit)) { throw new DistributedLockException(ZK 锁获取超时); } return task.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new DistributedLockException(获取锁被中断, e); } catch (Exception e) { throw new DistributedLockException(ZK 锁异常, e); } finally { try { if (mutex.isAcquiredInThisProcess()) { mutex.release(); } } catch (Exception e) { // 释放锁失败时记录日志不阻塞业务 log.warn(ZK 锁释放异常, e); } } } }3.3 分布式锁的幂等性保障/** * 幂等性校验——分布式锁的必要补充 * 为什么有了分布式锁还需要幂等校验 * 因为锁可能因 GC 停顿或网络超时意外释放 * 客户端重试时可能绕过已失效的锁导致重复执行 */ Component public class IdempotentService { private final RedisTemplateString, String redisTemplate; /** * 幂等校验基于请求唯一 ID 的去重 * 为什么用 SETNX 而非先查后写 * 因为先查后写在并发场景下存在竞态条件 * 两个请求可能同时查询到不存在然后都执行 */ public boolean checkAndMark(String requestId, Duration ttl) { String key idempotent: requestId; Boolean success redisTemplate.opsForValue() .setIfAbsent(key, 1, ttl); return Boolean.TRUE.equals(success); } public void executeIdempotent(String requestId, Duration ttl, Runnable task) { if (!checkAndMark(requestId, ttl)) { log.info(重复请求被拦截: {}, requestId); return; } task.run(); } }四、架构权衡三种分布式锁的选型决策Redis 锁的局限Redis 主从异步复制导致锁可能在主从切换时丢失。RedLock 算法通过多节点投票解决此问题但增加了延迟需向 5 个节点依次加锁。Antirez 与 Martin Kleppmann 的著名论战揭示了 Redis 锁在极端场景下的理论缺陷。在实际生产中如果业务能容忍极低概率的锁失效如非资金场景Redis 锁的性价比最高。ZK 锁的局限性能是 ZK 锁的最大短板。每次加锁需要 2-3 次网络往返创建节点 获取子节点列表 设置 Watch在低延迟要求的场景下不适用。ZK 集群的写入吞吐量受限于 Leader 节点通常在万级 TPS。此外ZK 的 Session 超时机制可能导致长 GC 停顿后锁被误释放。etcd 锁的局限etcd 在国内的使用生态不如 Redis 和 ZK 成熟客户端库和运维工具相对匮乏。etcd 的性能介于 Redis 和 ZK 之间适合已有 etcd 基础设施的 K8s 环境。选型建议非资金场景选 Redis性能优先资金相关场景选 ZK一致性优先K8s 原生环境选 etcd基础设施复用。无论选择哪种方案都必须配合幂等校验因为没有任何分布式锁能提供 100% 的互斥保证。五、总结分布式锁的实现需要在性能与一致性之间做出权衡。Redis 锁性能最优但存在极端场景下的锁丢失风险ZK 锁一致性最强但性能受限etcd 锁介于两者之间。生产环境中锁的选型应基于业务对一致性的容忍度资金场景必须用强一致性方案普通业务场景用 Redis 锁即可。无论选择哪种方案幂等性校验都是不可或缺的兜底机制。落地时建议封装统一的锁接口屏蔽底层实现差异便于后续根据业务需求切换锁方案。