Redis分布式锁面试总在过期时间上翻车?4个场景把加锁、续期和误删讲清

Redis分布式锁面试总在过期时间上翻车?4个场景把加锁、续期和误删讲清

从 SETNX 与 EXPIRE 的故障窗口,到 TTL 超时、误删和主从切换,拆清 Redis 分布式锁面试中最容易被追问的 4 个边界,并给出可直接复述的回答框架。

先记住这 4 句面试结论

如果你只回答「Redis 分布式锁就是 SETNX」,面试官大概率会继续追问:过期时间怎么设?业务执行超过 TTL 怎么办?释放锁时会不会误删?主节点挂了呢?
一套更稳的回答顺序是:
  1. 加锁要把占有条件和过期时间放进同一条命令SET lock:order:123 <唯一随机值> NX PX 30000
  2. 锁的值要能代表持有者:释放锁前先比对随机值,不能直接 DEL
  3. TTL 是锁的有效窗口,不是业务一定能完成的保证:任务可能超过 TTL,就要讨论续期、幂等和 fencing token,而不是盲目把 TTL 调大。
  4. 主从切换会改变安全边界:Redis 官方文档明确提醒,异步复制下,主节点写入的锁还没同步到副本就发生故障,副本提升后可能让另一个客户端再次拿到同一把锁。1
下面按 4 个真实追问场景拆开。每一段都要能落到命令、时序或故障证据上。

场景一:为什么不能先 SETNX,再 EXPIRE?

这是最常见的第一处翻车。
SETNX lock:order:123 client-token-a
EXPIRE lock:order:123 30
如果客户端执行完 SETNX 就崩溃,第二条 EXPIRE 没机会执行,这把锁可能永久留在 Redis 里。即使把两条命令放进代码的相邻位置,也没有消除这个故障窗口。
面试时直接说:占锁和设置 TTL 要合并成一次带条件的 SET
SET lock:order:123 client-token-a NX PX 30000
NX 表示只有 key 不存在时才设置,PX 30000 表示设置 30 秒过期时间。Redis 的 SET 文档把这两个选项列为同一条命令的条件和过期参数;设置成功返回 OK,条件不满足则不会设置。2
这里还有一个细节:client-token-a 不能写成固定字符串。每次加锁都要生成足够唯一的随机值,否则后面释放锁时无法确认「这把锁是不是我拿的」。Redis 官方示例也把随机值作为锁的签名,用来避免客户端误删别人的锁。1
面试官如果继续问「SETNX 还能不能用」,可以这样答:
老代码里可能会把 SETNX 和 EXPIRE 分开写,但新实现应优先用 SET key value NX PX ttl。如果业务必须拆开,就要补偿异常和清理逻辑;这不能改变两步之间存在故障窗口的事实。

场景二:业务执行超过 TTL,锁会发生什么?

假设 A 用 PX 30000 拿到锁,但订单处理在第 31 秒才完成。第 30 秒锁已经过期,B 可以重新拿到同一个 key。此时 A 如果还继续写共享资源,互斥关系已经失效。
这不是「把 TTL 改成 5 分钟」就解决的问题。只要任务耗时存在长尾,就始终可能碰到超时。你要先回答两个问题:
  • 业务能不能接受同一资源被重复处理?
  • 共享资源是否会检查操作版本或持有者身份?
如果任务确实可能超过 TTL,通常需要在锁仍由当前客户端持有时续期,并限制续期次数或总时长。Redis 的分布式锁文档提到,可以用脚本扩展锁的 TTL,但续期仍受有效期、节点成功数和时钟漂移等条件约束,不能把它当成无限期占锁。1
更稳的业务回答是:
TTL 负责自动释放,续期负责覆盖正常的长任务;两者都不能替代幂等。对扣库存、发放权益这类不能重复执行的操作,我还会在数据库或资源服务侧做条件更新,并考虑 fencing token,让过期客户端的旧请求即使晚到也不能覆盖新请求。
这里的 fencing token 可以理解成单调递增的操作编号。资源方只接受更大的编号,拿着旧锁的 A 即使从暂停中恢复,也不能把 B 已经提交的新状态覆盖掉。Redis 官方文档也把 fencing tokens 列为需要考虑的安全边界。1

场景三:为什么释放锁不能直接 DEL?

看一条故障时序:
  1. A 拿到锁,值是 token-a
  2. A 执行很慢,锁先过期。
  3. B 拿到同一个 key,值变成 token-b
  4. A 任务结束,执行 DEL lock:order:123
  5. B 的锁被 A 删除,第三个客户端又可能拿到锁。
所以释放锁必须满足「值还是我设置的 token」和「删除」同时成立。只先 GETDEL 也不够,因为两条命令之间仍可能发生过期和重新加锁。
Redis 官方文档给出的兼容写法是用 Lua 把比较和删除放在一次脚本执行里:
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
调用时,KEYS[1] 传锁的 key,ARGV[1] 传当前客户端自己的随机 token。值相等才删除,不相等就返回 01
面试回答不要只说「用 Lua 保证原子性」。把防住的故障说出来:Lua 防的是旧持有者误删新持有者的锁。它不能解决任务已经超过 TTL 后,旧客户端继续写业务数据的问题,后者要靠幂等、版本校验或 fencing token。
续期也要遵守同一个原则。不能只因为 key 还存在,就给它续命;必须确认 value 仍是当前客户端的 token,否则可能把别人的锁延长。

场景四:主节点挂了,从节点接管,锁还安全吗?

假设你说「Redis 做了主从高可用,所以锁不会丢」,面试官很可能就会追问这一幕:
  1. A 在 master 上成功加锁。
  2. master 还没把这次写入复制给 replica 就宕机。
  3. replica 被提升为新的 master。
  4. B 连接新 master,发现锁 key 不存在,于是成功加锁。
A 和 B 可能同时认为自己持有锁。Redis 官方分布式锁文档把这个结果称为安全性被破坏,原因是复制过程是异步的。1
因此,面试中的合格回答不应停在「加哨兵」「配集群」。你要先说明业务对重复执行的容忍度:
  • 可重试、可幂等的任务:Redis 单实例锁加 TTL,配合 token、比较删除、幂等键和失败重试,可能已经够用,但要说清故障窗口。
  • 不能重复扣款或覆盖关键状态的任务:锁只做并发控制,最终写入仍要经过数据库条件更新、版本号或 fencing token 校验,不能把 Redis 锁当成最后一道安全闸门。
  • 必须讨论多节点锁的系统设计题:可以继续谈 Redlock,但要同时说出它的前提和代价。官方文档要求在多数独立 Redis master 上成功加锁,并且获取锁的总耗时要小于有效期;网络分区、时钟漂移、节点重启和可用性损失都要纳入设计。1
这段回答的重点不在于背出一个方案名,而在于把「Redis 能保证什么」和「业务系统还要补什么」分开。

面试现场可以按这 4 步答

遇到「请设计一个 Redis 分布式锁」时,按下面顺序说,能显著减少被追问时的跳步:
  1. 先定资源和时间:锁住的是订单、用户、库存还是定时任务?预计耗时、最大耗时和允许重复执行的边界是什么?
  2. 再说获取:用唯一 token 配合 SET key token NX PX ttl,占锁和 TTL 一次完成;拿不到锁就按业务选择快速失败、退避重试或排队。
  3. 再说释放和续期:释放用 token 比对后原子删除;续期也要先确认 token,并设置总时长上限。
  4. 最后说故障:检查客户端崩溃、任务超时、主从切换、网络分区和旧客户端恢复;关键写入再用幂等、版本校验或 fencing token 兜底。
可以把答案压缩成这一段:
我会用唯一 token 通过 SET key token NX PX ttl 获取锁,避免 SETNX 和 EXPIRE 分离造成永久锁。释放时用脚本校验 token 后再删除,防止过期后的旧客户端误删新锁。任务可能超过 TTL 时做有上限的续期,但关键写入仍用幂等和版本校验保护。若依赖主从故障转移,我会说明异步复制可能造成锁丢失,再根据业务是否允许重复执行选择补充 fencing token 或更严格的一致性方案。

5 分钟自查清单

  • 是否把 SETNX + EXPIRE 的故障窗口讲清,而不只是背命令?
  • 是否知道 NX 是「key 不存在才设置」,PX 是毫秒级过期时间?2
  • 是否为每次加锁生成唯一 token?
  • 是否能解释为什么不能直接 DEL
  • 是否知道「GET 后再 DEL」也可能在两条命令之间出问题?
  • 是否区分了 TTL、续期、幂等和 fencing token 的职责?
  • 是否能用时序说明主从异步复制造成的锁丢失?
  • 是否能说出你的方案在任务超时、网络分区和节点重启时怎么退场?
如果最后只剩一句话,记住:分布式锁的难点不是把 key 写进 Redis,而是让过期、释放、故障转移和业务写入的边界都能被验证。

Related content

  • Sign in to comment.
More from this channel