写在前面
本文讲分布式锁——多个服务/节点之间如何互斥访问共享资源。它有几种经典实现(数据库、Redis、Zookeeper、etcd),各有优劣。理解它们的差异,才能在不同场景选对方案。
一、为什么需要分布式锁
1
2
3
4
5
6
7
8
9
10
11
| 单机:用 lock / Mutex / 信号量(进程内)
分布式:多个进程/机器要互斥
单机锁管不到其他机器
需要一个"全局可见"的锁
典型场景:
- 防止重复操作(下单、扣库存、发券)
- 限流(全局 QPS)
- 选主(只一个节点执行定时任务)
- 资源独占(同时只一个能改配置)
|
1
2
3
4
5
6
7
| 分布式锁的必要条件:
✓ 互斥(Mutual Exclusion)— 任意时刻只有一个客户端持有
✓ 避免死锁 — 持有者崩溃,锁要能自动释放
✓ 可重入(可选)— 同一持有者可重复获取
✓ 高可用 — 锁服务不能单点
✓ 高性能 — 加锁/解锁要快
✓ 公平性(可选)— 按请求顺序获取
|
二、实现方式对比
1
2
3
4
5
6
| 方式 一致性 性能 可靠性 复杂度 适用
──────────────────────────────────────────────────────────
数据库 强 中 中 低 简单场景
Redis 弱(AP) 极高 中 低 高并发、容忍偶尔失效
Zookeeper 强(CP) 中 高 中 强一致、可靠
etcd 强(CP) 高 高 中 云原生、K8s
|
三、数据库实现
1
2
3
4
5
6
7
8
9
10
| -- 方式一:唯一索引(加锁 = 插入,解锁 = 删除)
INSERT INTO locks(resource, owner, expire_at) VALUES('order:123', 'uuid', NOW()+10s);
-- 插入成功 = 获取锁;唯一冲突 = 被占
DELETE FROM locks WHERE resource='order:123' AND owner='uuid';
-- 方式二:悲观锁(SELECT ... FOR UPDATE)
BEGIN;
SELECT * FROM account WHERE id=1 FOR UPDATE; -- 加行锁
-- 业务
COMMIT;
|
1
2
3
| 优点:简单,不用引入新组件
缺点:性能差(DB 是瓶颈)、过期清理麻烦、FOR UPDATE 占连接
适用:并发不高、已有数据库、不想引入 Redis/ZK
|
四、Redis 实现
1
2
3
4
5
6
7
8
| # 加锁:SET key value NX EX(原子)
SET lock:order:123 <owner_uuid> NX EX 10
# NX:不存在才设置(互斥)
# EX 10:10 秒过期(防死锁)
# 返回 OK = 获取成功;nil = 已被占
# 解锁:必须验证 owner 再删(Lua 保证原子)
# 否则可能删掉别人的锁
|
1
2
3
4
5
6
| -- 解锁脚本(原子)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
|
Redis 锁的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
| 问题1:业务执行超过锁过期时间
锁到期自动释放,别的客户端拿到锁 → 两个客户端同时执行
解决:看门狗续期(定时延长过期时间)
Redisson 框架自带看门狗
问题2:Redis 主从切换丢锁
主节点加锁成功 → 还没同步到从 → 主挂了 → 从升主(没锁数据)→ 别人能加锁
解决:Redlock(向多个独立 Redis 实例加锁,多数成功才算成功)
问题3:Redlock 的争议
Martin Kleppmann 认为 Redis(AP)做锁不可靠
因为 GC 停顿、时钟漂移可能导致锁失效
Redis 作者 antirez 反驳(认为实际够用)
|
1
2
3
4
5
| Redis 锁的定位:
✓ 高性能、高并发
✗ 不是绝对可靠(AP 系统的特性)
适用:对偶尔失效容忍的场景(如限流、防重复提交)
不适用:绝对不能错的场景(如扣款、扣库存)→ 用 ZK/etcd
|
五、Zookeeper 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ZK 锁基于「临时顺序节点」+ Watch:
1. 创建顺序临时节点 /lock/node-0001, /lock/node-0002 ...
2. 判断自己是不是序号最小的
- 是 → 获得锁
- 否 → 监听前一个节点(只监听前一个,避免羊群效应)
3. 前一个节点删除(释放锁)→ 自己被唤醒 → 成为最小 → 获得锁
4. 持有者崩溃 → 临时节点自动删除 → 后继者被唤醒
特点:
✓ 强一致(ZAB 共识)
✓ 公平锁(顺序节点)
✓ 会话失效自动释放(临时节点)
✓ 无看门狗问题(会话心跳保活)
✗ 性能不如 Redis(CP,要多数确认)
|
1
2
3
| 羊群效应(Herd Effect):
✗ 所有等待者都监听同一个节点 → 释放时全部唤醒抢锁 → 惊群
✓ 改进:每个等待者只监听「前一个」节点 → 顺序唤醒
|
六、etcd 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
| etcd 锁基于 Lease(租约)+ 事务(TXN)+ Revision(全局递增版本):
1. 创建带 Lease 的 key(Lease 保活,崩溃自动过期)
2. 用事务获取所有锁 key,判断自己 Revision 是否最小
- 是 → 获得锁
- 否 → 监听前一个 key
3. 类似 ZK 的顺序等待
特点(类似 ZK):
✓ 强一致(Raft)
✓ Lease 自动过期(防死锁)
✓ 比 ZK 更轻量、性能更好、云原生标配
K8s 的分布式锁多用 etcd(或基于 etcd 的 leader election)
|
1
2
3
4
5
6
| // etcd 分布式锁示例(Go concurrency 包)
m, err := concurrency.NewSession(client)
l := concurrency.NewMutex(m, "/lock/order/123")
l.Lock(ctx) // 加锁
defer l.Unlock(ctx)
// 业务
|
七、CP vs AP 锁的本质区别
1
2
3
4
5
6
7
8
9
10
11
12
13
| Redis 锁(AP):
返回"加锁成功"时,可能主从还没同步
极端情况下锁可能失效(但你以为成功)
→ 快,但不可靠
ZK / etcd 锁(CP):
返回"加锁成功"时,数据已通过共识写入多数节点
强一致保证,不会假成功
→ 可靠,但慢
选择:
需要绝对可靠(金融、库存)→ CP(ZK/etcd)
容忍偶尔失效、追求性能 → AP(Redis)
|
八、使用建议
1
2
3
4
5
6
7
8
9
10
11
12
13
| 场景 推荐
──────────────────────────────────────────────────
高并发、容忍偶尔失效(限流) Redis
防重复提交、幂等 Redis
扣库存、扣款(绝对不能错) ZK / etcd / 数据库
选主(Leader Election) ZK / etcd
配置/资源互斥 etcd(云原生)/ ZK
通用建议:
优先用成熟框架(Redisson、curator、etcd concurrency)
别自己造轮子(边界情况极多)
锁要设过期时间 + 校验 owner
业务要考虑"锁失效"的兜底(幂等)
|
九、小结
- 必要条件:互斥、防死锁、高可用、(可选)可重入/公平
- 数据库锁:简单但性能差,适合低并发
- Redis 锁(AP):极高性能,但不绝对可靠;Redlock 有争议
- Zookeeper 锁(CP):顺序临时节点,强一致公平锁
- etcd 锁(CP):Lease + 事务,云原生首选
- 本质:CP 锁可靠慢,AP 锁快但可能假成功;按业务对可靠性的要求选
下一篇讲分布式事务——跨服务/数据库如何保证一致性。