文章背景图

分布式锁生产实践:Redis、数据库、ZooKeeper 与 Fencing Token

2026-08-16
0
-
- 分钟

# 分布式锁生产实践:Redis、数据库、ZooKeeper 与 Fencing Token

当多个进程同时修改同一份资源时,单机互斥锁已经无能为力。分布式锁看似只是“抢一个 Key”,实际必须处理租约过期、进程暂停、网络分区、主从切换和重复释放。本文从生产故障边界出发,说明 Redis、数据库和 ZooKeeper/etcd 锁应如何选择。

## 一、先确认是否真的需要锁

优先考虑无锁方案:

1. 数据库唯一约束防止重复创建;

2. 乐观锁版本号实现条件更新;

3. 幂等键避免请求重复执行;

4. 单分区消息队列保证同一业务键串行;

5. 状态机拒绝非法状态迁移。

锁会降低吞吐并扩大故障面。只有业务确实需要跨进程互斥,而且无法通过数据模型解决时再使用。

## 二、分布式锁必须满足什么

一个可用的锁至少需要:

- 互斥:正常情况下同一资源只有一个持有者;

- 所有权:只有持有者能释放自己的锁;

- 有界等待:持有者宕机后锁最终能够释放;

- 可观测:能看到持有者、剩余租约、等待量和失败量;

- 故障边界清楚:网络分区和暂停发生时,业务仍不会被旧持有者破坏。

需要特别注意:锁只约束所有遵守同一协议的参与者。有人绕过锁直接写数据库,锁就没有意义。

## 三、Redis 锁的正确基本姿势

获取锁应使用一个原子命令:SET resource_lock random_token NX PX ttl_ms。random_token 必须对每次获取都唯一,可使用足够随机的 UUID。

释放锁时不能直接 DEL。可能出现这样的故障:客户端 A 获得锁后发生长时间 GC 暂停;锁过期后客户端 B 获得新锁;A 恢复并执行 DEL,误删了 B 的锁。正确做法是用 Lua 脚本原子比较 token,相等才删除。

锁租期不能凭感觉设置。它要覆盖正常业务耗时的高分位数,并考虑网络抖动。任务可能超过租期时,需要受控续约;续约线程也可能暂停,因此续约不能成为正确性的唯一保证。

## 四、最危险的问题:租约过期后旧持有者仍在运行

即使释放逻辑完全正确,A 在暂停期间锁已过期,B 已获得锁并完成写入,A 恢复后仍可能继续写,从而覆盖 B。仅靠 Redis token 无法阻止目标资源接受旧写入。

对库存、主备切换、账务等关键操作,应增加 Fencing Token(栅栏令牌)

1. 每次成功获取锁时获得一个单调递增序号;

2. 持有者写目标资源时携带这个序号;

3. 目标资源记录已接受的最大序号;

4. 小于最大序号的迟到请求被拒绝。

例如 B 获得令牌 102 并写入后,恢复的 A 携带 101,数据库或下游服务直接拒绝。真正的安全来自“资源拒绝旧持有者”,而不只是锁服务认为谁当前持锁。

## 五、Redis 主从切换的边界

如果锁只写到主节点,还没复制到副本时主节点宕机,副本提升为新主后,另一个客户端可能再次获得同一个锁,形成双持有者。

因此对不能容忍并发执行的关键业务,不应把异步复制 Redis 锁当作唯一正确性屏障。可以使用支持共识和线性一致写的协调系统,或在目标资源上配合 Fencing Token、唯一约束和状态机兜底。

Redlock 试图通过多个独立 Redis 实例降低单点问题,但它仍依赖时间假设和租约语义。是否适用必须根据故障模型评估;对账务、主节点选举等高风险场景,仍应使用栅栏令牌或共识系统保护最终资源。

## 六、数据库锁适合什么场景

数据库方案包括:

- 行级锁或 SELECT ... FOR UPDATE;

- 唯一键抢占锁记录;

- 数据库提供的 advisory lock;

- 带 owner、expire_at 和版本号的租约表。

优点是可以和业务数据放在同一个事务中,语义清晰。缺点是锁竞争会占用连接、产生死锁或拖慢主库。

不要持有数据库事务后调用外部 HTTP 服务,否则网络延迟会延长锁时间。事务内只执行必要的短 SQL,外部操作应通过状态机和异步流程完成。

## 七、ZooKeeper 或 etcd 协调锁

ZooKeeper 可利用临时顺序节点实现公平锁:会话失效时临时节点删除,等待者只监听前一个节点,避免惊群。etcd 可利用 Lease、事务比较和修订版本实现锁与栅栏令牌。

这类系统基于共识,协调语义通常强于普通异步主从缓存,但并不代表客户端可以忽略会话失效。客户端一旦失去租约,就必须停止关键操作;目标资源仍建议校验修订号或栅栏令牌。

## 八、锁失败时的生产排查

### 获取不到锁

1. 查询锁持有者和剩余 TTL;

2. 检查持有任务是否卡在数据库、网络或外部接口;

3. 确认是否存在错误续约;

4. 检查锁粒度是否过大;

5. 查看获取延迟和超时率。

### 同一任务执行了两次

1. 按业务 ID 查所有实例日志和 Trace;

2. 核对 token、租期、获取与释放时间;

3. 检查是否发生 GC 暂停、容器冻结或网络分区;

4. 检查 Redis 主从切换或协调系统会话失效;

5. 验证业务是否具有幂等键、唯一约束和 Fencing Token。

不要看到双执行就简单延长 TTL。TTL 再长也无法消除进程暂停和网络分区,只会让真正宕机后的恢复更慢。

## 九、监控与演练清单

监控锁获取成功率、等待时间分位数、持有时间分位数、续约失败、异常释放、锁过期数量和当前等待者数量。上线前至少演练:进程被暂停超过 TTL、持有者宕机、网络双向中断、Redis 主从切换、协调节点重启、重复请求和迟到请求。

## 十、选型结论

- 低风险、短任务并且业务本身幂等:可使用规范的 Redis 租约锁;

- 与数据库事务强相关、竞争不高:优先数据库锁;

- 选主、配置发布等协调场景:ZooKeeper 或 etcd 更合适;

- 关键资源不能接受旧持有者写入:必须增加 Fencing Token 或目标端版本校验。

分布式锁不是正确性的终点。可靠设计应把锁、幂等、唯一约束、状态机和对账组合起来,并明确每一种故障发生后系统如何停止、恢复和验证结果。

原创

分布式锁生产实践:Redis、数据库、ZooKeeper 与 Fencing Token

本文链接: 分布式锁生产实践:Redis、数据库、ZooKeeper 与 Fencing Token

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

评论交流

文章目录