Redis 分布式锁的「误删」问题是什么?如何解决?
简化版
误删指一个客户端把别人持有的锁给删了。根源是「业务执行时间超过了锁的过期时间」:客户端 A 拿锁后业务卡住,锁到期自动释放,B 拿到了同一把锁,此时 A 缓过来执行 DEL 解锁,删掉的其实是 B 的锁。解决两步走:① 加锁时 value 存客户端唯一标识,解锁时先校验「这锁是不是我的」再删;② 用 Lua 脚本保证「校验 + 删除」原子执行。根治「业务超时」还要加自动续期(看门狗)。
详细版
误删是怎么发生的(时间线):
- 客户端 A
SET lock uuid-A NX EX 10,加锁成功,过期 10 秒。 - A 的业务执行了 12 秒(超过锁的 10 秒过期时间)。
- 第 10 秒,锁自动过期释放。
- 客户端 B
SET lock uuid-B NX EX 10,加锁成功(因为锁已释放)。 - 第 12 秒,A 业务终于跑完,执行
DEL lock——把 B 的锁删了! - 此时 B 还在执行业务,却已「裸奔」,其他客户端又能拿锁 → 并发失控。
解决方案:
- ① value 唯一标识:加锁时
value存客户端唯一值(UUID + 线程 ID)。解锁前先GET判断 value 是不是自己的,是自己的才删。上面场景里 A 解锁时发现锁的 value 是uuid-B不是uuid-A,就不删了。 - ② Lua 原子解锁:「GET 判断 + DEL」必须原子,否则判断完、删之前锁又恰好到期被别人拿走,仍会误删。用 Lua 脚本在 Redis 内原子执行。
- ③ 自动续期(治本):让锁在业务没做完时不过期,从根上避免「锁过期了业务还在跑」。
完整版教学
一、误删的本质:锁过期与业务未完成的错位
误删的根本原因不是解锁写错,而是锁的生命周期和业务的生命周期不匹配——锁提前过期了,业务还没做完。一旦锁提前释放,锁就可能被别人拿走,而原持有者浑然不觉,最后它的解锁操作就作用到了「下一任持有者」身上。所以解决误删要从两个层面:表层(解锁时验明正身,别删错)+ 根层(别让锁提前过期)。
二、表层防护:唯一 value + Lua
唯一 value:加锁 SET lock <uuid+threadId> NX EX 10。每个客户端(甚至每个线程)的 value 都不同。解锁逻辑变成「只删 value 等于我自己的锁」。这样即使 A 的锁已过期、被 B 拿走,A 去解锁时一比对 value 不是自己的,就不动手,避免删掉 B 的锁。
为什么还要 Lua:光有「先 GET 校验再 DEL」还不够,因为这是两条命令、非原子。存在这样的窗口:A GET 发现 value 是自己的(此刻锁还没到期)→ 就在 A 准备 DEL 的一瞬间,锁恰好到期、被 B 抢走 → A 的 DEL 又把 B 的删了。所以要把「判断 + 删除」塞进一个 Lua 脚本,Redis 单线程执行脚本期间不会插入别的命令,保证原子。
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
记忆点:唯一 value 解决「删的是不是我的锁」,Lua 解决「判断和删除之间会不会被插队」。两者配合才彻底防误删。
三、根层治本:自动续期(看门狗)
唯一 value + Lua 只能保证「不删错别人的锁」,但没解决「A 的锁提前过期、B 拿到锁,导致 A、B 同时在临界区」的并发问题——那才是更危险的。根治办法是自动续期:给锁一个初始过期时间(如 30s),起一个后台线程(看门狗),每隔一段时间(如 10s,即过期时间的 1/3)检查业务是否还在执行,如果还在就把锁的过期时间重新续到 30s。这样只要业务没结束,锁就一直不过期;业务结束或客户端宕机(看门狗线程也随之停止),锁才自然过期释放。Redisson 内置了这套看门狗机制。
四、过期时间到底该设多久
- 设太短:容易业务没做完就过期,触发误删/并发(要靠续期兜底)。
- 设太长:万一持锁者宕机,别人要等很久才能拿锁(可用性差)。
- 实践:设一个大于绝大多数业务执行时间的值(如 30s),并配合自动续期应对偶发的超长业务。不要拍一个刚好卡在业务耗时边缘的值。
五、误删锁问题落地自检
排查 Redis 锁误删时,要按时间线看锁的归属是否发生变化。典型事故是 A 在 T=0 加锁,TTL=10s,A 卡到 T=12 才执行 DEL;B 在 T=11 已经拿到同一个 key,A 的 DEL 删除的其实是 B 的锁。只要这条时间线能成立,就必须用唯一 value 加 Lua 原子校验释放。
六、常见误区与追问
这道题不能只背概念,要把「Redis 锁误删问题」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 误删发生在锁过期后被别人重新获取,而旧持有者执行 DEL 删除了新锁,解决靠唯一 value 和 Lua 原子校验删除 | 不要停在名词解释 |
| 流程机制 | A 加锁并超时 -> 锁过期自动释放 -> B 获取同一 key -> A 业务结束直接 DEL -> B 的锁被误删 -> Lua 校验 owner 后避免 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | A 锁 10 秒过期,业务跑 12 秒;B 在第 11 秒拿到锁;A 第 12 秒 DEL 就会删掉 B 的锁 | 分布式锁只能控制临界区进入,不能替代业务幂等和状态校验 |
Redis 锁误删问题 面试拆解:
1. A 加锁并超时
2. 锁过期自动释放
3. B 获取同一 key
4. A 业务结束直接 DEL
5. B 的锁被误删
6. Lua 校验 owner 后避免
记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「Redis 锁误删问题」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:锁过期后旧线程 DEL 没影响。 过期后的同名 key 可能已经属于新持有者。
- 误区:先 GET 再 DEL 就安全。 GET 和 DEL 两条命令之间可能被抢占,必须 Lua 原子执行。
- 误区:把 TTL 设很长就能解决。 过长会降低故障恢复速度,仍不替代 owner 校验。
- 追问:Lua 解锁脚本怎么写? if get(key)==value then del(key) else return 0 end。
- 追问:业务超时超过 TTL 怎么办? 用 watchdog 续期或设置合理 TTL,同时业务要幂等。
- 追问:误删会造成什么后果? 多个客户端同时进入临界区,互斥失效。
七、加强记忆
误删 = 客户端删了别人的锁,根因是「业务耗时 > 锁过期时间」导致锁提前释放、被他人获取,原持有者解锁时删错对象。解决三层:① value 存客户端唯一标识,解锁前校验是不是自己的;② 用 Lua 脚本原子执行「校验 + 删除」防插队;③ 自动续期(看门狗)从根上不让锁提前过期,同时也避免了 A、B 同时持锁的并发。生产用 Redisson 一次性解决这三点。