分布式锁深度解析:Redis、etcd 与 ZooKeeper 三种实现与选型指南

深度讲解分布式锁的原理与落地:为什么单机锁在分布式下失效、分布式锁的核心条件(互斥/安全/活性)、基于 Redis SETNX/Lua 的实现、基于 etcd 租约与 Revision 的实现、基于 ZooKeeper 临时顺序节点的实现、RedLock 与争议、锁的续期与看门狗、三种方案的对比与选型、常见避坑与最佳实践。

在分布式系统中,“同一份资源同时只能被一个节点操作"是再常见不过的需求:扣减库存、发放优惠券、幂等写入、定时任务防重。单机时代的 synchronized 与 ReentrantLock 只对单个 JVM 进程内的线程生效,一旦系统拆成多节点,锁就必须"跨进程、跨节点"工作——这就是分布式锁要解决的问题。本指南从原理出发,讲透 Redis、etcd、ZooKeeper 三种主流实现,并给出选型与避坑建议。

关键概念:分布式锁 = 在分布式环境中协调"互斥访问共享资源"的机制。一个合格的分布式锁必须同时满足三个条件:互斥性(任意时刻只有一个客户端持有)、安全性(锁释放后其他客户端才能获取,不会死锁)、活性(持有者崩溃后锁能被自动回收,其他客户端能继续获取)。


一、为什么需要分布式锁

1.1 单机锁的局限

在单机单进程时代,锁通过内存中的对象状态实现,JVM 内的所有线程共享同一把锁。

单机锁:
  synchronized / ReentrantLock
  → 作用于「单个 JVM 进程」内的线程
  → 进程内互斥有效

问题:系统拆成 N 个节点后
  - 每个节点各自 JVM 内互斥,节点间不互斥
  - 两个节点可以同时执行同一段临界区代码
  → 需要「跨进程」的互斥机制 = 分布式锁

典型的并发问题场景:分布式定时任务在多个节点同时触发,如果没有锁,同一批数据会被重复处理;秒杀扣库存时多个节点同时执行"查库存-减库存”,就会超卖。

1.2 分布式锁的核心条件

一个可靠的分布式锁,业界通常要求满足以下几点:

条件含义不满足的后果
互斥性任一时刻只有一个客户端持锁并发执行临界区,数据被破坏
安全性持锁者才能释放,释放后他人可取锁被误删,出现"插队"
活性崩溃/超时后锁自动回收持锁者宕机,系统永久死锁
公平性(可选)等待者按先来后到获取饥饿、羊群效应

ℹ️ 核心:分布式锁本质上是在一个"共享的、高可用的"存储上做一次原子性的"占用标记"。它的可靠性上限,取决于底层存储的共识能力。


二、基于 Redis 的分布式锁

2.1 最简单的 SETNX 实现

Redis 2.6.12 之后提供了原子的 SET key value NX EX ttl,这是实现分布式锁的最小内核。

# 加锁:仅当 key 不存在时设置,并带过期时间(原子操作)
SET lock:order product-01 NX EX 30
# 返回 OK → 加锁成功;返回 nil → 锁已被占用

# 业务处理...

# 释放:必须校验 value(自己的标识)后才能删,防止误删别人的锁
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else
  return 0
end

value 用 UUID/业务标识,是为了防止"把别人的锁删掉":如果 A 持锁超时被释放,B 拿到锁,A 业务结束直接 del 会把 B 的锁误删。所以删除前必须用 Lua 脚本比较 value。

2.2 看门狗(Watchdog)自动续期

业务处理时间超过 TTL,锁就会在业务执行中被自动释放,造成并发进入。解决思路是"锁快过期时自动续期"——Redisson 的看门狗机制就是干这个的。

看门狗机制(Redisson):
  - 默认锁超时 30s,看门狗每 10s 检测一次
  - 若业务未结束,自动把锁续期到 30s(续命)
  - 业务结束 → 主动释放锁 → 看门狗停止
  → 既防止业务过长锁被误释放,又防止持锁者宕机锁永不过期

自定义方案:
  用一个后台线程定时执行 Lua:若 value 匹配则 EXPIRE 续期
  → 续期前确认锁仍属于自己,否则停止续期

2.3 主从切换的丢锁问题

Redis 分布式锁最大的隐患:主从异步复制。加锁写入主节点后,主节点宕机,从节点提升为主节点,但锁还没来得及同步——锁"丢了",另一个客户端可以再次拿到锁。

问题场景:
  1. A 在主节点加锁成功
  2. 主节点宕机,锁未复制到从节点
  3. 从节点晋升为主节点,B 加锁成功
  → A 和 B 同时持锁,互斥被打破

应对:
  - 业务上容忍极低概率的双执行(幂等兜底)
  - 或使用 RedLock(多节点投票,下面讲)
  - 或直接改用 etcd/ZooKeeper(强一致存储)

三、基于 etcd 的分布式锁

etcd 是强一致(Raft 共识)的键值存储,天然解决 Redis 主从复制丢锁的问题。etcd 分布式锁借助 租约(Lease)+ Revision + 前缀 Watch 实现。

3.1 原理

基于 etcd 的分布式锁(etcdv3 API):
  1. 创建租约 Lease(如 10s),自动续约
  2. 用租约关联一个 Key:client 在指定前缀下写入
     /lock/xxx/client-1(带 lease id)
     → 只有写入成功且 Revision 最小的那个 client 持锁
  3. 客户端 Watch 前缀 /lock/xxx/
     → 当前持锁者释放/超时后,下一个 Revision 最小者获锁
  4. 释放 = 删除自己的 Key(或租约过期自动删除)

Revision(全局单调递增版本号)保证了公平性:
  先写入者 Revision 小 → 先获锁

etcd 的 Key 是持久化的,但关联了租约,租约过期 Key 会被自动删除——这保证了"持锁者宕机,锁自动回收",满足活性要求。etcd 用 Raft 在多数节点间达成一致,不存在主从切换丢锁的问题。

3.2 用 etcd 实现分布式锁的要点

核心要点:
  - 用 Lease + KeepAlive 保持锁活跃,业务结束 revoke 租约
  - 用前缀 + Revision 实现公平排队(FIFO)
  - Watch 前一个持锁者,避免所有等待者同时抢(羊群效应)
  - 释放用事务 CompareAndSwap:确认 Revision 是自己才删除

与 Redis 锁的关键差异:
  - etcd 是强一致 → 无丢锁窗口
  - 但吞吐低于 Redis,锁获取延迟略高
  - 适合对一致性要求高的场景(配置、选主、分布式事务协调)

四、基于 ZooKeeper 的分布式锁

ZooKeeper 用 ZAB 协议保证顺序一致性,其**临时顺序节点(EPHEMERAL_SEQUENTIAL)**天然适合做分布式锁。

4.1 原理

基于 ZooKeeper 的分布式锁:
  1. 在锁节点 /lock 下创建临时顺序子节点 /lock/seq-0000000001
  2. 检查自己是不是序号最小的节点
     → 是 → 获得锁
     → 否 → 监听前一个(序号比自己小且最接近的)节点
  3. 前一个节点释放(删除/会话超时)→ 触发通知 → 再检查自己
  4. 获得锁的节点 = 序号最小者;释放 = 删除自己的节点

临时节点的特性:
  - 客户端会话断开 → 临时节点自动删除 → 锁自动释放
  → 持锁者宕机不会死锁(活性)
  → 且会话必须靠心跳维持,网络抖动时锁可能短暂释放

4.2 与 etcd 对比

两者都是"强一致协调服务",锁的实现思路同构:ZooKeeper 用会话 + 临时顺序节点,etcd 用租约 + Revision。差异主要在协议与生态:

维度ZooKeeperetcd
一致性协议ZABRaft
锁的载体临时顺序节点Lease + Revision Key
自动释放会话超时删临时节点租约过期删 Key
公平性序号 FIFORevision FIFO
运维独立集群,偏重与 Kubernetes 同源,轻量

五、RedLock:多节点投票锁的争议

Redis 官方曾提出 RedLock,在 N 个独立 Redis 节点上"多数派加锁"来缓解单点主从丢锁问题。它备受争议,Martin Kleppmann 与 Salvatore Sanfilippo 有著名论战。

5.1 RedLock 原理

RedLock 加锁流程(N 通常为 5):
  1. 对每个独立 Redis 实例依次 SET key value NX EX ttl
  2. 记录总耗时,若满足:
     加锁成功的实例数 > N/2,且总耗时 < TTL
     → 加锁成功
  3. 释放:对所有实例执行删除(Lua 校验 value)

思想:
  用「多数派」抵御单点故障
  → 即便个别节点宕机/丢锁,多数派仍持锁

5.2 争议与结论

RedLock 存在几个理论缺陷:依赖各节点时钟(时钟跳变会破坏判断)、无法处理"客户端 GC 暂停期间锁已过期"、多数派加锁仍有竞态窗口。业界主流观点:

实践结论:
  - 若要求"绝对互斥",RedLock 并不能提供数学上的保证
  - 若业务可以容忍极小概率双执行(配合幂等兜底),
    单 Redis + 看门狗通常够用
  - 若业务绝对不能接受双执行(扣款、转账),
    建议直接上 etcd/ZooKeeper 或分布式事务

六、三种方案对比与选型

维度RedisetcdZooKeeper
一致性主从异步(弱)Raft 强一致ZAB 顺序一致
吞吐最高(10w+ QPS)中(万级)中(万级)
加锁延迟最低(亚毫秒)低中(需建连接/监听)
自动释放TTL / 看门狗租约会话超时
公平排队需自实现Revision 天然序号天然
丢锁风险主从切换有窗口无会话抖动短暂释放
运维成本低(已有 Redis)中高(独立集群)
典型场景秒杀、缓存防击穿选主、配置、事务协调老系统协调服务

选型建议:

高并发、可容忍极小概率双执行
  → Redis + 看门狗(最常见,成本最低)

高并发且绝对不容忍双执行
  → etcd(云原生标配)或 ZooKeeper

已有 ZooKeeper/etcd 集群,不想多维护一套
  → 直接用现有协调服务做锁

补充:无论哪种锁,业务侧都应做「幂等兜底」
  → 锁只是降低并发概率,幂等保证最终正确

ℹ️ 核心:锁的选型本质是"一致性要求 vs 性能要求"的权衡。记住一个原则:能用幂等兜底解决的问题,不必强上强一致锁。


七、常见避坑

坑现象对策
忘记设 TTL持锁者宕机永久死锁加锁必带过期时间
TTL 太短业务未完锁被释放看门狗自动续期
删锁不校验 value误删他人锁Lua 脚本校验后删除
主从切换锁丢失双执行强一致存储或幂等兜底
长事务持锁锁成为瓶颈缩小临界区,锁内不做 IO
等待端全部自旋羊群效应用监听/Watch 排队唤醒
只靠锁不幂等极端情况仍出错业务侧幂等兜底

八、最佳实践清单

□ 加锁用 SET key value NX EX 原子命令(Redis)
□ value 用业务唯一标识,释放前 Lua 校验
□ 必设 TTL + 看门狗续期,防死锁防误释放
□ 临界区尽量小,锁内不做重 IO
□ 绝对互斥场景选用 etcd/ZooKeeper
□ 高并发场景配合幂等兜底(唯一索引/状态机)
□ 加锁失败采用监听唤醒而非盲目自旋
□ 定期检查锁的超时设置与业务耗时匹配度

一句话原则

分布式锁 = 互斥 + 防死锁 + 防误删,
选型上「性能优先用 Redis、一致性强求用 etcd/ZK」,业务侧永远配幂等兜底。

小结

分布式锁的本质,是在一个共享的高可用存储上做"原子占用标记"。Redis 用 SET NX EX + 看门狗 提供最高吞吐,但主从切换存在丢锁窗口;etcd 与 ZooKeeper 凭借强一致的 Raft/ZAB 天然消除丢锁,代价是吞吐与运维成本。落地时记住五件事:加锁必带 TTL、释放必校验 value、绝对互斥用强一致存储、临界区尽量小、业务侧始终配幂等兜底。当你能区分"可以容忍极小概率双执行"和"绝对不能双执行"两类场景时,分布式锁就不再是玄学,而是一个清晰的技术决策。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「distributed-systems」更多文章

  1. 分布式数据库前沿深度解析:TiDB、Spanner 与 CockroachDB 的共识与事务实现
  2. 异地多活与容灾架构深度解析:同城双活、两地三中心与多活设计
  3. 幂等设计与消息可靠性:不丢不重、防止重复消费的分布式基石