分布式锁
多实例共享资源需要跨进程的锁。Redis 分布式锁是面试高频题,考点集中在:正确写法、释放的原子性、续期、以及 RedLock 的争议。
从 SETNX 到正确写法
最朴素的实现:SETNX lock_key 1 成功即拿到锁,完事 DEL 释放。两个致命问题:
- 没有过期时间:持有锁的进程挂了,锁永远不释放(死锁)
- 释放不安全:A 的锁过期后 B 拿到锁,A 的 DEL 把 B 的锁删了
正确写法是一个原子命令:
SET lock_key unique_value NX PX 30000NX:key 不存在才设置(拿锁)PX 30000:30 秒自动过期(防死锁)unique_value:每个客户端唯一标识,释放时校验
释放必须用 Lua 保证原子(比较再删,两步分开会有竞态):
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0过期时间的两难
锁的过期时间设短了:业务没执行完锁就过期,另一个客户端进来,临界区被并发执行。设长了:客户端挂了锁要等很久。
解法是续期(看门狗):
看门狗续期机制
Redisson 是 Java 的事实标准实现:锁默认 30 秒过期,看门狗线程每 10 秒续期;支持可重入、等待超时、公平锁。其他语言有对应客户端。
RedLock 争议
RedLock(红锁):向多个独立 Redis 节点依次加锁,多数成功才算拿到,声称解决单点故障下的锁可靠性。
Martin Kleppmann 的批评(《How to do distributed locking》):
- 锁过期后旧持有者还在执行,无法阻止它写入共享资源
- GC 停顿(STW)可能让持有者“暂停”到锁过期之后,此时另一个客户端拿到锁,两边同时执行
- 结论:分布式锁的语义需要** fencing token**(令牌机制)才能真正防并发写,Redis 锁做不到
antirez 的回应:分布式锁解决的是“降低并发概率”而非数学上的绝对互斥;fencing token 依赖存储端配合,多数场景不需要。
工程结论:单点 Redis + 看门狗是绝大多数场景的务实选择;RedLock 复杂度高收益存疑,需要绝对安全时考虑 ZooKeeper/etcd 锁(临时节点 + 顺序节点 + 会话心跳,天然带 fencing 能力)。
面试追问
- SETNX 为什么不够? 没有过期时间会死锁;释放要校验持有者,两步操作有竞态。正确写法是 SET NX PX + Lua 释放
- 看门狗是什么? 持有期间定时续期的后台线程。业务没跑完锁不过期,进程挂了续期停止锁自动释放
- 释放锁为什么要 Lua? 比较 value 和 DEL 要原子,否则可能出现删掉别人锁的竞态
- RedLock 的争议? Kleppmann 指出锁过期和 GC 停顿会让两个客户端同时执行,需要 fencing token 才真正安全。antirez 认为概率足够低
- 什么场景用 ZooKeeper 锁? 对绝对互斥有要求(如金融写操作),Redis 锁的过期语义不够。ZK 的临时节点会话失效自动释放