← 返回题目列表

如何用 Redis 实现分布式锁?为什么用 SET NX EX 而不是 SETNX + EXPIRE?

高频 中等 第 4 / 26 题 更新于 2026/07/28
Redis分布式锁SETNX原子性

简化版

Redis 实现分布式锁的核心是用一条原子命令去「占坑」:SET lockKey uniqueValue NX EX secondsNX 保证只有 key 不存在时才能设置成功(互斥),EX 同时设置过期时间(防止持锁者宕机导致死锁),uniqueValue 是每个客户端唯一的标识(保证「谁加锁谁解锁」)。不能用老写法 SETNX + 单独 EXPIRE 两条命令,因为两条命令之间若客户端崩溃,锁就没了过期时间、变成永久死锁——必须用一条命令把「加锁」和「设过期」原子完成。

详细版

正确加锁

SET lock:order:123 uuid-abc NX EX 30
  • NX(Not eXists):key 不存在才设置成功 → 实现互斥(只有一个客户端能设成功)。
  • EX 30:30 秒后自动过期 → 防死锁(持锁者宕机,锁也会自动释放)。
  • uuid-abc:客户端唯一标识(如 UUID + 线程 ID)→ 解锁时校验,防止误删别人的锁。

正确解锁(必须用 Lua 保证「判断 + 删除」原子):

if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
else
  return 0
end

为什么不用 SETNX + EXPIRE 两条命令

SETNX lock 1      -- 加锁成功
(此刻客户端崩溃 / Redis 重启)
EXPIRE lock 30    -- 这条没执行!

结果:锁被设置了但没有过期时间,持锁客户端又崩了 → 这把锁永远不会释放,其他人永久拿不到,形成死锁。所以必须用 SET ... NX EX 一条命令原子完成。

完整版教学

一、加锁的三个要素缺一不可

一条 SET key value NX EX seconds 同时解决了三件事:

  1. 互斥(NX):并发下只有一个客户端能 SET 成功,其余返回 nil,实现「同一时刻一个持锁者」。
  2. 防死锁(EX):万一持锁客户端拿到锁后就宕机了,没机会主动解锁,靠过期时间兜底自动释放,别人还能拿到。
  3. 身份标识(value 唯一):value 存一个客户端唯一值,为解锁时「验明正身」做准备——只有锁的主人才能删它。

二、为什么两条命令不行(原子性)

老教程常写 SETNX lock 1 成功后再 EXPIRE lock 30。问题是这两条命令不是原子的,中间有个时间窗口。如果客户端在执行完 SETNX、还没执行 EXPIRE崩溃/被 kill/网络断,那把锁就永远没有过期时间。持锁的客户端已经死了,不会来解锁,这把锁就永久占用,所有后续请求都拿不到锁——死锁。Redis 2.6.12 之后 SET 命令支持 NXEX/PX 参数,把两步合成一条原子命令,彻底消除这个窗口。

易错点:面试问 Redis 分布式锁,如果只答 SETNX 会被追问过期时间问题。标准答案必须是 SET key value NX EX,并说明「原子地同时加锁和设过期」。

三、为什么解锁要用 Lua

解锁不能简单地 DEL lock,因为可能误删别人的锁。场景:客户端 A 拿到锁,业务执行超时,锁到期自动释放;此时客户端 B 拿到了这把锁;然后 A 业务终于跑完,执行 DEL lock——把B 的锁删了!所以解锁必须两步:先 GET 看 value 是不是自己的,是自己的才 DEL。但「GET 判断 + DEL」两步又不是原子的(判断完、删之前锁可能刚好到期被别人拿走),所以要用 Lua 脚本把「判断 value + 删除」放在 Redis 里原子执行。

四、这个方案还没解决的问题

SET NX EX + Lua 解锁是单机 Redis 分布式锁的正确基础版,但还有两个进阶问题它没覆盖:

  1. 锁提前过期:业务执行时间可能超过锁的过期时间,锁到期了业务还没做完,别人就能拿锁,导致并发。解决:自动续期(看门狗),Redisson 会后台定时给锁续命。
  2. 主从切换丢锁:单 Redis 是主从架构时,锁写到主节点还没同步到从节点,主就挂了,从升主,锁丢失,两个客户端同时持锁。解决:Redlock 算法或改用 ZooKeeper(这些有争议/权衡,见对应题目)。

所以生产一般不手写,而用 Redisson,它封装了唯一 value、Lua 解锁、看门狗续期、可重入等。

五、Redis 锁落地自检

Redis 锁上线前要把加锁和解锁命令写成固定模板:加锁用 SET key value NX PX ttl,解锁用 Lua 比较 value 后删除。value 应该是请求级唯一 ID,而不是用户 ID 或线程名;ttl 要略大于业务最长耗时,例如业务 P99 是 800ms,可以先设 3 到 5 秒,再结合监控调整。

六、常见误区与追问

这道题不能只背概念,要把「Redis 分布式锁基础」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。

回答层次要讲清的内容容易漏掉的边界
核心结论Redis 锁核心是 SET key value NX PX ttl 原子加锁,Lua 校验 value 后删除来安全解锁不要停在名词解释
流程机制客户端生成唯一 value -> 执行 SET NX PX -> 成功后进入临界区 -> 释放时 Lua 比较 value -> 匹配才 DEL说明触发方、参与方、状态变化和兜底
工程取舍SET lock:sku request-123 NX PX 30000 表示只在不存在时加锁,并设置 30 秒过期分布式锁只能控制临界区进入,不能替代业务幂等和状态校验
Redis 分布式锁基础 面试拆解:
1. 客户端生成唯一 value
2. 执行 SET NX PX
3. 成功后进入临界区
4. 释放时 Lua 比较 value
5. 匹配才 DEL

记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「Redis 分布式锁基础」这道题,不要把相邻概念混成一段泛泛的分布式套话。

  • 误区:先 SETNX 再 EXPIRE 也一样。 两条命令非原子,进程在 EXPIRE 前宕机会造成死锁。
  • 误区:DEL key 就能释放锁。 锁过期后可能被别人获得,直接 DEL 会误删他人锁。
  • 误区:Redis 锁一定强一致。 单 Redis 有单点,主从异步复制故障切换可能丢锁。
  • 追问:为什么 value 要唯一? 用于证明锁归属,释放时防止误删别人的锁。
  • 追问:为什么用 Lua 解锁? 比较 value 和删除 key 要原子执行。
  • 追问:过期时间如何设置? 略大于业务最大执行时间,长任务配合续期或拆分。

七、加强记忆

Redis 分布式锁:加锁用一条原子命令 SET key uniqueValue NX EX seconds——NX 保互斥、EX 防死锁、value 唯一保「谁加谁解」;解锁用 Lua 脚本原子地「GET 校验 value 是自己的才 DEL」,防止误删别人的锁。绝不能用 SETNX+EXPIRE 两条命令(中间崩溃会留下无过期时间的死锁)。这是基础版,还需看门狗续期解决业务超时、Redlock/ZK 解决主从丢锁,生产用 Redisson。