引言
本文为 How to do distributed locking 的简要笔记,作者为Martin Kleppmann(DDIA作者)。
一、分布式锁的使用目的
- 效率(Efficiency)
- 避免重复工作(如耗时计算、重复发邮件)。
- 锁失效的后果:只是增加一些成本或带来轻微不便。
- 正确性(Correctness)
- 确保并发进程不会破坏共享状态。
- 锁失效的后果:可能导致数据损坏、丢失、不一致,甚至严重错误(如医疗场景)。
如果仅为效率,可以使用单个 Redis 实例即可;如果为正确性,Redlock 并不合适。
二、分布式锁的常见问题
锁与进程暂停
- 客户端持有锁后可能因 GC 暂停 或 进程调度/内存缺页 等原因被长时间挂起。
- 锁是 租约(lease),有过期时间。
- 当租约过期后,另一个客户端可能获取锁,而原客户端恢复后依然执行写操作 → 数据损坏。
网络延迟
- 网络包可能被延迟很久(如 GitHub 曾出现 90 秒延迟事件)。
- 延迟到达的写请求可能在租约过期后才到达,导致冲突。

三、解决方案:Fencing Token(栅栏令牌)
原理
- 每次获取锁时生成 递增的令牌号。
- 存储服务在处理写请求时必须携带并检查令牌号:
- 确保即使旧客户端恢复,也不会覆盖新客户端的结果。

关键点
- 需要锁服务保证 单调递增 的令牌。
- ZooKeeper 提供的 zxid 或 znode 版本号 可直接作为 fencing token。
Redlock 的问题
- Redlock 仅依赖随机数,无法保证单调递增 → 无法提供 fencing token。
- 如果要实现,几乎需要一个共识算法来生成令牌。
四、Redlock 的缺陷
时间依赖性
- Redis 键过期基于
gettimeofday,可能跳跃(NTP 校时、人工修改时钟)。
- 算法假设:
- 网络延迟有限且小于 TTL。
- 进程暂停时间有限且小于 TTL。
- 时钟误差有限。
- 这些假设 在实际系统中不可靠。
示例场景
- 时钟跳跃
- 节点 C 时钟前跳 → 提前过期 → 客户端 2 获得锁 → 客户端 1 和 2 同时认为自己持锁。
- 进程暂停
- 客户端 1 在请求中途被 GC 挂起 → 锁过期 → 客户端 2 获得锁 → 客户端 1 恢复后仍认为自己持锁。
模型问题
- Redlock 实际依赖 同步系统模型(bounded delay/clock/pause)。
- 现实中只能假设 部分同步 或 异步 + 故障检测。
- 在异步模型下,Redlock 无法保证安全性。
五、对比与替代方案
Redlock
- 对效率型场景过重(需要 5 个 Redis 实例,多数投票)。
- 对正确性场景又不安全(缺少 fencing token,依赖不合理的时间假设)。
建议方案
- 仅效率需求
- 使用单实例 Redis(
SETNX 获取锁,DEL 检查值释放锁)。
- 明确标注为“近似锁”,可能偶尔失效。
- 需要正确性
- 使用 ZooKeeper / etcd / Consul 等 共识系统。
- 使用 Curator 提供的锁实现。
- 强制在资源访问时使用 fencing token。
六、结论
- Redlock “不上不下”:
- 效率场景:太复杂、不值。
- 正确性场景:不安全、不可用。
- 本质缺陷:
- 缺少 fencing token。
- 假设过于依赖同步系统模型。
- 正确做法:
- 效率优化 → 单节点 Redis。
- 正确性保障 → ZooKeeper/共识系统 + fencing token。