分布式锁
用途:跨进程互斥(防重复执行、防并发写冲突)时正确实现与使用
最小可用实现
「最小可用实现」小节链接SET lock:<resource-id> <token> NX PX 30000... 业务 ...EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 <key> <token>四个不可省略的细节:
NX+ 过期时间必须原子(SET ... NX PX),分两步会在中间崩溃时留下永久锁。- token 唯一(随机串/线程标识),解锁必须校验持有者,否则会删掉别人的锁。
- 解锁用 Lua,保证“比较 + 删除”原子。
- 超时时间 > 业务最坏耗时,否则锁提前失效导致并发。
持锁时间不确定的问题
「持锁时间不确定的问题」小节链接- 看门狗续期:客户端后台线程周期性延长 TTL(Redisson 默认每 10 秒续 30 秒),业务完成或进程退出后停止续期。
- Fencing token(护栏令牌):锁服务返回单调递增序号,写入时携带序号,存储端拒绝旧序号的请求。这是唯一能防“锁失效后旧持有者继续写”的严谨做法——Redis 单靠自身难以提供。
红锁(Redlock)的争议
「红锁(Redlock)的争议」小节链接Redlock 用 N 个独立 Redis 实例多数派加锁,试图降低单点故障影响。Martin Kleppmann 的批评要点:GC 停顿、时钟漂移与网络延迟仍会让“过期锁”被误认为有效;因此 Redlock 不适合当作正确性保证,只适合在容忍偶发重复的场景提升可用性。Antirez 的反驳强调其设计目标与假设。
工程结论:要正确性就用带 fencing 的共识系统(如基于 etcd/ZooKeeper 的租约,或数据库唯一约束/条件更新),要效率与可用性用单实例锁 + 幂等设计。
使用边界
「使用边界」小节链接| 需求 | 更合适的做法 |
|---|---|
| 防重复提交/重复消费 | 幂等键 + 唯一约束(不要依赖锁) |
| 库存/额度扣减 | DB 条件更新(WHERE x>=n)或原子计数 |
| 定时任务多实例互斥 | 调度中心的分片与租约机制 |
| 跨节点串行化写 | 队列单消费者,或分片保证同一 key 落同一节点 |
| 保护一段慢计算 | 用“singleflight + 结果缓存”而不是锁 |
常见坑
「常见坑」小节链接| 坑 | 后果 | 对策 |
|---|---|---|
| 锁粒度按方法而非资源 | 无关请求互相阻塞 | key 用资源 ID |
| 业务在锁内做长 IO | 续期失败或超时后并发 | 缩小临界区、异步化 |
| Redis 主从切换丢锁 | 两个客户端同时持锁 | 接受并靠幂等兜底,或用共识存储 |
| 可重入需求未实现 | 自死锁 | 用 hash 结构记录持有者与计数 |
| 忘记 finally 解锁 | 锁滞留到 TTL | 模板化封装(tryLock/unlock 成对) |
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
| 有分布式锁就线程安全 | 只保护了跨进程互斥,进程内仍需正确设计 |
| 用锁替代幂等 | 锁会失效,幂等是更强的保证 |
| TTL 设很长以防超时 | 崩溃后锁长期滞留,恢复变慢;应配合看门狗 |
| 认为锁能保证“恰好一次” | 无法保证,仍需业务去重 |
延伸阅读
「延伸阅读」小节链接- Redis 官方文档:SET 的 NX/PX、分布式锁模式页
- Martin Kleppmann《How to do distributed locking》与 Antirez 的回应
- Redisson 官方文档(可重入锁、看门狗、读写锁、联锁)