跳转到内容

分布式锁

用途:跨进程互斥(防重复执行、防并发写冲突)时正确实现与使用

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>

四个不可省略的细节:

  1. NX + 过期时间必须原子(SET ... NX PX),分两步会在中间崩溃时留下永久锁。
  2. token 唯一(随机串/线程标识),解锁必须校验持有者,否则会删掉别人的锁。
  3. 解锁用 Lua,保证“比较 + 删除”原子。
  4. 超时时间 > 业务最坏耗时,否则锁提前失效导致并发。
  • 看门狗续期:客户端后台线程周期性延长 TTL(Redisson 默认每 10 秒续 30 秒),业务完成或进程退出后停止续期。
  • Fencing token(护栏令牌):锁服务返回单调递增序号,写入时携带序号,存储端拒绝旧序号的请求。这是唯一能防“锁失效后旧持有者继续写”的严谨做法——Redis 单靠自身难以提供。

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 官方文档(可重入锁、看门狗、读写锁、联锁)