经典案例:分布式锁与高并发互斥系统 (Distributed Lock - Redis Redlock 与 ZooKeeper)¶
在单体应用时代,多线程并发争抢共享资源时,我们使用编程语言内置的锁(如 Java 的 synchronized / ReentrantLock,C++ 的 std::mutex)就能完美保证互斥性。
然而,在现代微服务与分布式架构中,一个服务通常会被部署在 数十台完全独立的服务器或 Docker 容器 中。单机内存锁只能锁住当前操作系统的进程,对其他服务器上的进程完全视而不见!
分布式锁(Distributed Lock)的核心任务是:跨越不同的物理服务器与网络边界,为全网所有节点提供统一的、高可用、高可靠的互斥访问控制。
1. 初学者视角:为什么单机锁在分布式环境下彻底失效?¶
graph TD
subgraph "单机锁 (同一物理机内存)"
Thread1[线程 1] & Thread2[线程 2] --> Mutex["JVM / OS 内存锁 (synchronized / mutex)"]
Mutex --> Res1[本地共享内存]
end
subgraph "分布式环境 (多台独立服务器)"
ServerA["服务器 A (本地加锁成功)"] --> DB[(MySQL / 集中式数据库)]
ServerB["服务器 B (本地加锁也成功!)"] --> DB
NoteNode["两台机器各自认为自己拿到了锁<br>同时执行扣减余额操作, 发生严重数据错乱!"]
end
分布式锁必须依赖一个所有服务器都能共同访问的第三方中心化组件(如 Redis、ZooKeeper、Etcd)来协调加锁状态。
2. 需求分析与核心设计目标 (Requirements)¶
在设计一个合格的分布式锁时,必须严格满足以下四个核心特性:
- 互斥性 (Mutual Exclusion - 绝对排他):在任何时刻,全网只能有一个客户端持有锁;
- 防死锁 (Deadlock Free):即使持有锁的客户端在执行中途崩溃宕机,锁也必须能在一定时间后自动释放,绝不能永远卡死;
- 解铃还须系铃人 (Safe Release):客户端 A 加的锁,绝不能被客户端 B 误删除;
- 高容错与高可用 (Fault Tolerance):锁服务本身不能因为单点故障而导致全站业务瘫痪。
3. 基于 Redis 的分布式锁演进与避坑指南¶
Redis 凭借纯内存极速读写(单机可达 10 万 QPS),是互联网大厂实现分布式锁最常用的方案。但实现一个真正健壮的 Redis 锁,需要经历数次演进与填坑:
3.1 初代版本(死锁漏洞)¶
- 致命缺陷:SETNX 和 EXPIRE 是两条独立命令,非原子操作!如果服务器在执行完 SETNX 之后、执行 EXPIRE 之前突然断电或重启,这个锁就永远没有过期时间,变成永久死锁!
3.2 进阶版本:原子加锁与随机值绑定¶
从 Redis 2.6.12 开始,官方合并了原子参数:
# 原子操作:设置键值 + 仅在不存在时设置 (NX) + 过期时间 30 秒 (PX 30000)
SET lock_key "random_request_id_abc123" NX PX 30000
为什么 Value 必须设置随机唯一 ID(如 UUID)?¶
如果所有的客户端加锁时 Value 都写固定值 "1":
1. 客户端 A 抢到锁,设置了 30 秒超时;
2. 客户端 A 遇到网络卡顿或慢 SQL,执行了 35 秒(此时锁在第 30 秒已经自动过期被 Redis 删除);
3. 客户端 B 趁机成功获取到了锁;
4. 客户端 A 终于执行完毕,执行 DEL lock_key;
5. 灾难发生:客户端 A 把客户端 B 刚刚持有的锁给强行删掉了!
正解:释放锁时必须核验 UUID,且必须使用 Lua 脚本保证核验与删除的原子性!
-- 释放锁的原子 Lua 脚本
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
3.3 业务超时痛点:Redisson 看门狗 (Watchdog) 自动延期¶
上述方案依然存在一个硬伤:如果业务还没执行完,锁的 30 秒超时时间就到了,怎么办?
- 如果把超时时间设得很短(如 5 秒),业务稍有抖动就会被提前释放引发并发错乱;
- 如果把超时时间设得极长(如 10 分钟),一旦客户端挂了,其他节点必须干等 10 分钟。
Redisson 看门狗机制(自动续期): - 客户端加锁成功后,后台自动启动一个定时守护线程(Watchdog); - 每隔 10 秒(默认超时 30 秒的三分之一)检查一次当前线程是否还在执行; - 若业务仍在进行,看门狗向 Redis 发送命令,将锁的过期时间重新重置为 30 秒; - 当业务正常结束释放锁,或者进程意外挂掉(看门狗线程也随之死亡),锁将在 30 秒后自然过期,优雅解决死锁与超时矛盾。
4. 分布式系统大师之辩:Redlock 算法与 Martin Kleppmann 质疑¶
如果 Redis 采用单主多从(Master-Slave)架构,当主节点加锁成功后、尚未同步给从节点就突然宕机,从节点被选举为新主节点时,锁就会凭空丢失!
为了解决该问题,Redis 创始人提出了 Redlock 算法: - 部署 5 个完全独立的 Redis 主节点(彼此不构成主从); - 客户端依次向 5 个节点发起加锁请求; - 只要在规定时间内成功锁住大于半数(\(\ge 3\) 个)节点,才判定加锁成功。
graph TD
Client[客户端] --> Node1["Redis Master 1 (成功)"]
Client --> Node2["Redis Master 2 (成功)"]
Client --> Node3["Redis Master 3 (成功)"]
Client --> Node4["Redis Master 4 (超时失败)"]
Client --> Node5["Redis Master 5 (成功)"]
Note["5 节点中 4 节点成功 (>= 3 超过半数), 判定加锁成功!"]
分布式权威大师 Martin Kleppmann 的经典质疑:¶
著名分布式学者 Martin Kleppmann 发表长文指出:在没有物理绝对时间保障的异步网络中,Redlock 并不安全! 1. 长时间 GC 停顿(STW):客户端 A 拿到锁后发生长时间垃圾回收停顿,暂停期间锁已超时被客户端 B 拿到; 2. NTP 时钟跳变:运维手动调整服务器时钟或 NTP 同步突变,导致锁瞬间提前过期。
终极防御:Fencing Token (递增围栏令牌)¶
- 存储层(如 MySQL)维护一个单调自增的
version; - 每次获取分布式锁时,系统下发一个严格自增的数字令牌(如 Token = 101);
- 写入数据库时带上校验:
UPDATE account SET balance = ... WHERE token > last_token; - 哪怕客户端 A 因为 GC 停顿苏醒后拿着过期的 Token 101 尝试写入,也会被数据库直接拒绝!
5. 基于 ZooKeeper 的分布式锁 (CP 强一致性首选)¶
与 Redis 偏向 AP(高可用、高性能)不同,ZooKeeper 是典型的 CP 系统(强一致性),天然适合作为高可靠分布式锁。
5.1 核心原理:临时顺序节点与监听机制¶
sequenceDiagram
participant C1 as 客户端 1
participant C2 as 客户端 2
participant C3 as 客户端 3
participant ZK as ZooKeeper (/lock 节点)
C1->>ZK: 创建临时顺序节点 node_001
ZK-->>C1: 返回 node_001 (序号最小, 获得锁!)
C2->>ZK: 创建临时顺序节点 node_002
Note over C2,ZK: C2 发现自己不是最小, 监听 node_001
C3->>ZK: 创建临时顺序节点 node_003
Note over C3,ZK: C3 发现自己不是最小, 监听 node_002
Note over C1: C1 业务执行完毕, 删除 node_001
ZK-->>C2: 触发 Watcher 事件通知 C2!
Note over C2: C2 成为当前最小序号, 获得锁!
为什么不会产生“羊群效应 / 惊群现象 (Herd Effect)”?¶
如果 1000 个客户端都在争抢锁,当锁释放时,如果同时唤醒 1000 个节点,ZooKeeper 会瞬间被海量事件打垮。
ZooKeeper 巧妙解法:每个节点只监听紧挨着自己前面的那一个节点(node_002 只监听 node_001,node_003 只监听 node_002)。当锁释放时,只有一个客户端被精确唤醒,性能极度平稳!
6. Redis vs ZooKeeper 分布式锁选型对比¶
| 维度 | Redis 分布式锁 (如 Redisson) | ZooKeeper 分布式锁 (如 Curator) |
|---|---|---|
| CAP 定理定位 | AP 模型(优先保证高可用与吞吐) | CP 模型(优先保证绝对强一致性) |
| 性能与吞吐量 | 极高(每秒数十万 QPS,纯内存计算) | 中等(写操作需 Paxos/ZAB 强共识落盘) |
| 死锁防范 | 依靠过期时间 TTL + 看门狗自动续期 | 依靠临时节点(Session 断开自动消失) |
| 主从切换风险 | 异步主从复制存在极小概率丢锁风险 | 绝对零丢锁(必须半数以上节点写入成功) |
| 适用业务场景 | 电商高并发抢购、防重复提交、用户流水 | 核心金融转账、批处理调度、对一致性要求苛刻的场景 |