适用读者:用过
SETNX做锁、但不确定"锁会不会锁错人"的人。
前置:S05-B03(锁)、S12-J02。
实测环境:Redis 7.0.15(2026-11-05)。
覆盖旧文:868(84) 869(78) 1576(355) 1569(297) 1575(240) 2681(220) = 1,274 点。
覆盖扩展:E-S12-007(误删与 Lua 校验)、E-S12-008(RedLock 争议)。
上一篇:S12-J02 · 缓存与数据库一致性
下一篇:S12-J04 · 事务、Lua 与 Pipeline。
1. 一句话结论
一把安全的 Redis 锁要同时满足三条:原子抢锁(SET NX PX)、唯一持有者标识、校验后才释放。缺第三条,就会把别人的锁删掉。
实测三条:
客户端A: OK
客户端B: ← nil 表示抢不到(1)
锁过期后 B: OK;当前持有者: B (2)
A 的 Lua 防误删(value=A): del=0(正确:拒绝删除)
A 的错误做法(裸 DEL): del=1(B 的锁被误删!)(3)
2. 第一代:SETNX + EXPIRE(有 bug)
SETNX lock 1
EXPIRE lock 10 # ← 两条命令不是原子的
问题:SETNX 成功后进程崩溃 → EXPIRE 没执行 → 死锁(key 永不过期)。
正确写法(Redis 2.6.12+):
SET lock <uuid> NX PX 10000 # 原子:抢锁 + 过期一起
实测(第 1 条):A 拿到 OK,B 拿不到(nil)。
3. 第二代:误删事故与 Lua 校验(E-S12-007)
场景:A 的锁过期了(业务耗时 > TTL),B 抢到;A 完成后执行 DEL lock → 删掉了 B 的锁。
实测:
当前持有者: B
A 的防误删 Lua(value=A): del=0 ← 正确拒绝
A 的错误做法(裸 DEL): del=1 ← B 的锁被误删
锁是否还在: 0
| 做法 | 结果 |
|---|---|
裸 DEL |
删错人 |
if get(key)==myValue then del |
两次命令非原子,仍有窗口 |
| Lua 脚本 | ✅ 原子:比较 + 删除 |
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
这就是"锁的 value 必须是唯一标识(UUID/线程 ID)"的原因——没有标识就无法校验。
4. 第三代:Redisson 解决了什么(A-1576)
| 能力 | 说明 |
|---|---|
| 看门狗(watchdog) | 自动续期(默认 30s 续到业务结束) |
| 可重入 | 内部计数 |
| 阻塞等待/超时 | tryLock(waitTime, leaseTime, unit) |
| 公平锁/读写锁 | 多种实现 |
"看门狗会不会无限续期":客户端进程死了就不再续 → 锁最终会释放(这是它比"手动长 TTL"更安全的地方)。
判据:"业务耗时不确定吗?" 不确定 → 用看门狗(Redisson),别手动猜 TTL。
5. RedLock 的争议(E-S12-008)
方案:向 N(通常 5)个独立 Redis 实例申请锁,过半成功才算拿到。
| 争议点 | 说明 |
|---|---|
| 依赖时钟 | 各节点时钟漂移会影响安全性 |
| GC 停顿 | 客户端长停顿后"以为自己还持有锁" |
| 运维复杂 | 5 个独立实例 |
| 作者与反对者 | 分布式系统领域有公开论战(细节待核,属学术争论) |
共识:单实例 Redis 锁在"主从切换"时可能丢锁(异步复制,A01 §3);RedLock 试图缓解,但代价高、仍有理论争议。
判据:"锁错了会怎样?" 会出钱/出数据 → 用强一致协调服务(ZK/etcd)或数据库约束兜底,而不是只靠 Redis 锁。
6. 五个误区
| 误区 | 真相 |
|---|---|
"SETNX 就够了" |
缺过期会死锁,缺 value 会误删 |
| "锁的 value 随便写" | 必须唯一标识 |
"裸 DEL 释放没问题" |
实测会删掉别人的锁 |
| "Redisson 一定安全" | 主从切换仍可能丢锁 |
| "RedLock 是银弹" | 有争议,且运维复杂 |
7. 自检
- 安全锁的三个条件?
SETNX+EXPIRE的两个 bug?- 实测里裸
DEL删掉了谁的锁? - 看门狗为什么会"自愈"?
- RedLock 的两种质疑?
8. 下一篇
S12-J04 · 事务、Lua 与 Pipeline
锁讲完,下一篇讲"多命令怎么保证连续/原子":MULTI/EXEC、Lua、Pipeline 的边界。
本篇属于系列《S12 · Redis 与缓存》。旧文来源:A-868/869/1576/1569/1575/2681;扩展点 E-S12-007/008。抢锁/误删/校验为 Redis 7.0.15 实测。