Distributed Locks Implemented Right
Redis SETNX and Redlock, database locks, Zookeeper/etcd locks, watchdog renewal and common pitfalls.
What a Distributed Lock Must Guarantee
- Mutual exclusion: one holder at a time.否则没有意义
- No deadlock: a crash releases the lock (expiry / session).
- Fairness (optional): FIFO under contention.
- Crash-safe for both holder and locker: TTL plus renewal take care of the failure modes.
Redis Lock and its Traps
# 单实例正确姿势:SET key value NX PX <ttl>
# value = 唯一标识(UUID),解锁时必须先比对再删(Lua 保证原子)
加锁:
token = uuid()
SET lock:order:7 $token NX PX 10000
续期(看门狗):
后台线程在 TTL/3 时检查持有者仍是自己则续期
Redisson 的 watchdog 默认 30s ttl、10s 续期
解锁(Lua 原子):
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else return 0 end
常见错误:
✗ SETNX + EXPIRE 两步(中途崩溃锁不过期)
✗ 解锁直接 DEL(删了别人的锁)
✗ 业务超过 TTL 未续期 → 锁过期被他人抢到 → 并发
✗ 把 Redis 锁当强一致锁(主从切换瞬间可能双持)
Redlock
Redlock:多实例多数派(N≥5,加锁 ≥ N/2+1 成功且耗时 < TTL)
对 5 个独立 Redis 同时申请:
成功数 ≥ 3 且剩余有效时间足够 → 锁定成功
任意一步失败 → 全部解锁重试
争议(Martin Kleppmann):
依赖系统的时钟同步;GC 期 pause、时钟跳变可能双持
作者(antirez)反驳:实际工程中足够好
结论(实用主义):
对「防止重复操作」这类业务级互斥 → Redis 锁 + 看门狗,足够
对「钱、库存等强正确性」→ 数据库唯一键 / ZK / etcd 类 CP 系统
If losing a lock translates into money lost, do not use Redis locks: use a CP system (Zookeeper, etcd) or a database constraint.
Database and CP System Locks
数据库方案(最简单、可靠):
唯一键约束表:INSERT lock(name, owner, expire_at)
借助唯一索引实现互斥,崩溃靠 expire_at 过期
SELECT ... FOR UPDATE:事务内行锁,事务结束即释放
Zookeeper / etcd:
创建临时顺序节点 / lease 会话
会话断开自动删除(无死锁安全)
顺序节点 = 天然公平队列
etcd lease(TTL) + txn 原子比较版本号
对比:获取锁后干完活就走用 CP 锁;长时间持锁用续期;
极高吞吐 + 容忍极小风险用 Redis;正确性第一用 ZK/etcd分布式锁必须满足什么
- 互斥:同一时刻只有一个持有者。
- 无死锁:持有者崩溃锁也能释放(过期/会话失效)。
- 公平性(可选):高争用时先到先得。
- 持有方与加锁方都可能崩溃:靠过期时间加续期覆盖故障路径。
Redis 锁及其陷阱
# 单实例正确姿势:SET key value NX PX <ttl>
# value = 唯一标识(UUID),解锁时必须先比对再删(Lua 保证原子)
加锁:
token = uuid()
SET lock:order:7 $token NX PX 10000
续期(看门狗):
后台线程在 TTL/3 时检查持有者仍是自己则续期
Redisson 的 watchdog 默认 30s ttl、10s 续期
解锁(Lua 原子):
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else return 0 end
常见错误:
✗ SETNX + EXPIRE 两步(中途崩溃锁不过期)
✗ 解锁直接 DEL(删了别人的锁)
✗ 业务超过 TTL 未续期 → 锁过期被他人抢到 → 并发
✗ 把 Redis 锁当强一致锁(主从切换瞬间可能双持)
Redlock
Redlock:多实例多数派(N≥5,加锁 ≥ N/2+1 成功且耗时 < TTL)
对 5 个独立 Redis 同时申请:
成功数 ≥ 3 且剩余有效时间足够 → 锁定成功
任意一步失败 → 全部解锁重试
争议(Martin Kleppmann):
依赖系统的时钟同步;GC 期 pause、时钟跳变可能双持
作者(antirez)反驳:实际工程中足够好
结论(实用主义):
对「防止重复操作」这类业务级互斥 → Redis 锁 + 看门狗,足够
对「钱、库存等强正确性」→ 数据库唯一键 / ZK / etcd 类 CP 系统
如果锁失效等于真金白银损失,别用 Redis 锁:上 CP 系统(Zookeeper、etcd)或数据库唯一约束。
数据库锁与 CP 系统锁
数据库方案(最简单、可靠):
唯一键约束表:INSERT lock(name, owner, expire_at)
借助唯一索引实现互斥,崩溃靠 expire_at 过期
SELECT ... FOR UPDATE:事务内行锁,事务结束即释放
Zookeeper / etcd:
创建临时顺序节点 / lease 会话
会话断开自动删除(无死锁安全)
顺序节点 = 天然公平队列
etcd lease(TTL) + txn 原子比较版本号
对比:获取锁后干完活就走用 CP 锁;长时间持锁用续期;
极高吞吐 + 容忍极小风险用 Redis;正确性第一用 ZK/etcd