Redis 分布式锁:SET NX、看门狗续期与 Redlock 争议
更新时间:2026-09-18。分布式锁用于「跨进程互斥」:定时任务别在多个节点重复执行、同一订单别被并发扣两次。Redis 实现轻量高性能,但用错了会出现「我删掉了别人的锁」「业务没做完锁先过期」,而红锁(Redlock)的安全性至今有争议。本文把正确实现、续期机制和「Redis 锁到底能不能用于强一致」的边界讲清。Redis 高可用背景见 Redis 主从与哨兵。
本文要回答的问题
- 最朴素的
SETNX+EXPIRE有什么竞态?正确的原子写法是什么? - 为什么删除锁要带唯一 token 并用 Lua?会误删什么?
- 业务执行超过锁过期时间怎么办?看门狗如何续期?
- Redlock 解决什么、Martin Kleppmann 为什么质疑它?
- 真正要强一致(防脑裂双执行)应该用什么?fencing token 是什么?
一、单机锁的正确实现
# 一条命令同时完成:不存在才设置 + 带过期时间(原子)
SET lock:order:1001 <唯一token> NX PX 30000三个必要条件:① NX(不存在才成功,实现互斥);② PX 毫秒(必须和 SET 同一条命令带过期,避免 SETNX 成功后客户端崩溃导致死锁——老写法分两条命令在中间崩溃就永久不释放);③ value 是本客户端唯一 token(UUID),释放时只删自己的。
释放必须「判断 token + 删除」原子,用 Lua:
-- 释放锁:只有持有者才能删
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end为什么需要 token:
A 拿锁(30s) -> 业务卡顿/GC/慢SQL 超过30s -> 锁自动过期
B 拿到同一把锁 -> A 缓过来执行 DEL,删掉的是 B 的锁!
-> C 又能拿锁,互斥被破坏
带 token:A 删之前比对,发现持有者已不是自己,拒绝删除二、过期时间与看门狗续期
设短了业务没做完锁就没了,设长了持有者宕机后别人要等很久。解法是看门狗(watchdog)自动续期(Redisson 默认实现):

Redisson 加锁不指定 leaseTime 时启动看门狗(默认锁 30s、每 10s 续到 30s);显式指定 leaseTime 则不续期、到期必释放。可重入锁用 Hash 存「线程→重入次数」,读写锁、信号量、CountDownLatch 也都有实现。
三、Redlock:多节点多数派及其争议
主从架构下有个漏洞:客户端在 master 拿到锁,锁还没同步到 replica,master 宕机,replica 提升为新主,另一个客户端又能拿到同一把锁——锁丢了。
Redlock(Antirez 提出)试图在无主、N(通常 5)个独立 Redis 节点上解决:向所有节点 SET NX PX,统计在多数(≥3)节点成功且总耗时小于锁有效期,才算加锁成功;释放时向所有节点删除。它不依赖主从复制。
Martin Kleppmann 的质疑(分布式系统角度):Redlock 的正确性依赖各节点时钟不发生大跳变、GC/网络停顿可控,但它本质仍是「带过期时间的锁」,无法区分「我还持有锁」和「锁其实已因停顿而过期」。在有 GC pause、时钟漂移、进程可能长时间停顿的现实里,它给人「强一致锁」的错觉,却不能提供带 fencing 的资源保护。
四、正确的安全姿势: fencing token
如果被保护资源是「必须绝对不能并发」的(写共享存储、扣款、向外部系统发起不可撤销操作),单靠过期式锁不够,需要 fencing token(单调递增的令牌):
1) 每次获取锁得到一个单调递增 token: 33, 34
2) 操作资源时带上 token
3) 资源端拒绝旧 token:
客户端A GC 停顿, 锁过期; B 拿到 token=34 先写
A 恢复后带 token=33 写 -> 资源发现 33<已见34 -> 拒绝能做 fencing token 的通常是 **ZooKeeper(zxid/事务顺序)或 etcd(revision)**这类共识系统,以及带版本号 CAS 的数据库。Redis 自身不提供单调递增且与锁绑定的 fencing 保证,这是 Redlock 争议的核心。
五、选型与坑
| 需求 | 选择 |
|---|---|
| 防重复执行、允许极小概率失败(定时任务、幂等可兜底) | Redis 锁(SET NX PX + token + Lua),Redisson 看门狗 |
| 已有主从、能接受故障切换极端丢锁 | Redisson 普通锁即可,配合业务幂等 |
| 对正确性要求极高、要 fencing/强一致 | ZooKeeper/etcd,或数据库唯一约束/乐观锁版本号 |
| 简单的「同一资源串行」且资源就在 DB | 唯一键、SELECT ... FOR UPDATE、乐观锁版本号,别外引锁 |
常见坑:① SETNX 与 EXPIRE 分两条命令(用一条 SET ... NX PX);② 释放不带 token、不用 Lua,误删他人锁;③ 锁粒度写错(用可变参数当 key 导致锁不住或锁太大);④ 业务在锁内做远程调用又不续期,超时丢锁;⑤ 拿到锁就不做幂等,把锁当唯一防线——锁是效率手段/降低并发,数据库唯一约束与幂等才是最后正确性保障(配合 接口幂等设计);⑥ Redlock 部署要真正独立的 N 节点,假独立(同机/同集群)没有意义。
跨服务一致性与最终一致的取舍见 分布式事务方案怎么选、Seata AT/TCC;Redis 缓存一致性见 缓存与数据库一致性。
总结
Redis 分布式锁:单机正确写法是一条 SET key token NX PX 租约 加锁、释放用 Lua 比对唯一 token 后删除,避免 SETNX/EXPIRE 分两步的死锁和误删他人锁;业务可能超过租约时用 Redisson 看门狗每 1/3 周期续期、客户端宕机则停续自动释放,需要可重入用 Hash 计数;主从切换可能丢锁,Redlock 用 N 个独立节点多数派加锁缓解但因依赖时钟且无 fencing token,安全性在强一致场景仍有争议——真正不能并发的扣款/外部不可逆操作要用 ZK/etcd 的单调 fencing token 或数据库唯一约束+乐观锁兜底,Redis 锁定位为高效互斥与降低并发,必须配合业务幂等而非充当唯一正确性防线。