Redis 分布式锁怎么实现?有哪些坑?
简化版
Redis 分布式锁通常用 SET lock value NX PX expire 实现,加锁要原子设置唯一标识和过期时间,解锁要用 Lua 脚本先校验 value 再删除。主要坑是误删别人的锁、锁过期业务没执行完、Redis 主从切换导致锁安全性下降,以及锁不能替代数据库唯一约束和幂等。
详细版
基本实现:
SET lock:order:1 random-value NX PX 30000
含义:
NX:key 不存在才设置,保证互斥;PX 30000:设置过期时间,避免客户端崩溃后死锁;random-value:唯一标识锁归属,释放时防止误删别人的锁。
解锁必须校验 value 后再删除,并且这两步要原子执行,常用 Lua:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
生产中还要考虑自动续期、锁超时、重试退避、业务幂等和失败补偿。
完整版教学
一、为什么不能只用 SETNX
早期很多人写:
SETNX lock 1
EXPIRE lock 30
这有致命问题:SETNX 成功后,如果客户端还没来得及执行 EXPIRE 就崩溃,锁永远不会过期。
所以加锁必须用一条原子命令完成“不存在才设置”和“设置过期时间”:
SET lock value NX EX 30
或用毫秒级 PX。
这道题的第一层就是“加锁动作必须原子”。SETNX 和 EXPIRE 分两条命令,中间只要客户端崩溃、网络断开或进程被杀,就会留下永不过期的锁。SET key value NX PX 30000 把互斥和过期时间绑定成一个命令,至少能避免最基础的死锁风险。
| 写法 | 是否推荐 | 问题 |
|---|---|---|
SETNX 后 EXPIRE | 不推荐 | 两步之间失败会留下死锁 |
SET NX PX | 推荐基础写法 | 原子设置锁和过期 |
| 没有唯一 value | 不推荐 | 解锁可能误删别人锁 |
| 没有 TTL | 不推荐 | 持锁客户端崩溃后无法释放 |
二、为什么 value 必须唯一
假设客户端 A 拿到锁,业务执行很久,锁过期了。客户端 B 又拿到同一个锁。此时 A 终于执行完,如果它直接 DEL lock,就会把 B 的锁删掉。
所以 value 必须是唯一随机值,代表锁归属。释放锁时先判断 value 是不是自己的,再删除。
时间线更清楚:
T0: A 加锁成功,value=A-uuid,TTL=30s
T31: A 还没执行完,锁过期
T32: B 加锁成功,value=B-uuid
T33: A 执行 DEL lock
结果:B 的锁被 A 误删
如果解锁时先校验 value,A 会发现当前锁不属于自己,就不会删除。value 通常用 UUID、请求 ID 或线程唯一标识,不能所有客户端都写同一个固定值。
三、为什么解锁要 Lua
解锁逻辑有两步:
- 读取锁 value;
- 如果等于自己的 value,就删除。
如果这两步分开执行,中间可能发生锁过期并被别人重新获取,仍然有误删风险。Lua 脚本在 Redis 中作为一个整体执行,可以保证这段逻辑不被其他命令插入。
Lua 的作用不是让 Redis 锁更“高级”,而是保证“比较 value + 删除 key”这两个动作不可被穿插。客户端先 GET 再 DEL,哪怕校验了 value,也可能在两条命令之间发生过期和重入。把逻辑放进服务端脚本后,Redis 会按单个脚本原子执行。
记忆钩子:加锁用一条
SET NX PX,解锁用一段 Lua;加锁防死锁,解锁防误删。
四、锁过期和自动续期
锁必须设置过期时间,但过期时间太短会导致业务没执行完锁就释放;太长又会让故障恢复变慢。
常见做法是估算业务最大执行时间,或者使用带看门狗续期能力的客户端库。续期也不是万能的,如果业务线程卡死、GC 暂停、网络抖动,都可能影响锁的正确释放。
比如业务 P99 执行时间是 800ms,偶发外部调用可能到 5s,那么 TTL 设置 1s 就很危险,设置 60s 又会让异常恢复很慢。自动续期可以在业务正常运行时延长 TTL,但如果应用长时间 STW、网络隔离或线程卡死,续期仍可能失败。锁超时必须配合业务幂等和最终状态校验。
五、Redis 锁的安全边界
单 Redis 实例锁在主从切换时有风险。比如主库加锁成功但还没复制到从库,主库宕机,从库提升为主库,另一个客户端可能再次加锁成功。
如果业务对锁安全性要求极高,要评估 Redlock、ZooKeeper、数据库约束、状态机、幂等控制等方案。很多业务里,Redis 锁只是降低并发冲突的手段,最终仍要靠数据库唯一索引、条件更新或幂等兜底。
例如防重复创建订单,Redis 锁可以降低并发进入临界区的概率,但最终仍应有数据库唯一索引约束 order_no 或业务幂等表。因为锁可能超时、误判、主从切换丢失或客户端重试,关键一致性必须在事实存储层兜住。面试时把“锁是优化并发控制手段,不是最终正确性唯一来源”讲出来很加分。
六、重试、阻塞和可重入边界
加锁失败后不要无脑死循环重试,应设置最大等待时间、退避间隔和失败降级。否则热点锁下大量客户端频繁 SET NX,Redis 压力也会上升。是否需要可重入锁要看业务,如果同一线程会重复进入同一临界区,就要保存重入计数和持有者身份,普通 SET NX PX 本身不支持可重入。
加锁建议:
生成唯一 value
-> SET lock value NX PX ttl
-> 成功才执行业务
-> 失败按退避重试或快速失败
-> Lua 校验 value 后释放
七、常见误区与追问
- 误区:
SETNX加锁后再EXPIRE就足够安全。 两条命令不是原子操作,中间失败会留下无过期锁。 - 误区:解锁直接
DEL key就行。 锁可能已经过期并被别人获取,直接删除会误删其他客户端的锁。 - 误区:TTL 设置得越长越安全。 TTL 太长会让故障恢复慢,太短会导致业务没执行完锁就过期,需要结合执行时间和续期策略。
- 误区:Redis 锁能替代数据库唯一约束。 Redis 锁有主从切换、超时和网络边界,关键正确性仍要靠数据库约束和幂等。
- 追问:为什么解锁用 Lua? 因为判断 value 和删除 key 必须原子执行,避免两步之间锁过期并被别人重新获取。
- 追问:Redis 主从切换为什么影响锁安全? 加锁写入可能还没复制到从库,主库宕机后从库提升,新客户端可能再次获取同一把锁。
八、加强记忆
Redis 分布式锁的基础公式是 SET key value NX PX ttl:NX 保证互斥,PX/EX 防止死锁,value 标识锁归属。释放锁必须用 Lua 先校验 value 再删除,防止误删别人锁。TTL、续期、重试、主从切换都有边界,所以 Redis 锁只能降低并发冲突,关键业务还要用唯一约束、条件更新、状态机和幂等兜住最终正确性。