60 秒回答模板

Redis 分布式锁的基础做法是加锁时执行 SET lockKey uniqueValue NX PX expireMs,NX 保证 key 不存在才成功,PX 设置过期时间避免持锁进程崩溃后死锁。uniqueValue 必须能标识锁持有者,例如请求 ID 或进程加线程唯一值。释放锁时不能直接 DEL,因为自己的锁可能已经过期并被别人重新获得;应该用 Lua 在 Redis 内原子执行“先比较 value 是否等于自己的 uniqueValue,再删除”。Lua 的价值不是单纯减少网络请求,而是让 GET 和 DEL 之间没有竞态。进一步还要说明锁超时、续期、可重入、主从切换和 Redlock 的适用边界。

考点 Redis 原子操作
难度 Java 后端高频题
回答目标 防死锁和防误删

深入解析

01

加锁必须是一条原子命令

SET key value NX PX ttl 把不存在才写入和设置过期时间合在一次执行里。老式 SETNX 后再 EXPIRE 中间如果进程崩溃,可能留下没有过期时间的死锁。

02

value 是锁的持有者凭证

value 不能随便写固定字符串。它要唯一标识本次加锁请求,释放锁、续期和排查问题都要用它判断当前客户端是否仍是锁持有者。

03

解锁要比较后删除

直接 DEL 的风险是客户端 A 执行业务太久,锁过期后客户端 B 获得锁,随后 A 执行 DEL 把 B 的锁删掉。正确做法是 value 相等才删除。

04

Lua 保证复合操作原子性

GET 和 DEL 如果分两次发,中间可能穿插其他客户端操作。Lua 脚本在 Redis 单线程执行模型下连续完成比较和删除,避免检查后状态变化。

05

过期时间和业务耗时要匹配

TTL 太短会导致业务未完成锁先释放,TTL 太长会拖慢故障恢复。长任务需要 owner 校验下的续期机制,并在业务结束或失败时停止续期。

06

分布式锁不是强一致事务

单 Redis 主从异步复制下,主节点写锁后未同步就故障,新的主节点可能没有锁记录。高可靠场景要评估 Redlock、fencing token、数据库约束或业务状态机兜底。

lua

释放锁的 Lua 最小脚本

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

return 0
  • KEYS[1] 是锁 key,ARGV[1] 是本次加锁时写入的唯一 value。
  • 脚本只在 value 匹配时删除,避免锁过期后误删其他客户端的新锁。
  • 加锁仍应使用 SET key value NX PX ttl,而不是 SETNX 后单独 EXPIRE。

易错点

  • 只用 SETNX 加锁,不设置原子过期时间,进程崩溃后可能永久占锁。
  • 解锁直接 DEL,不校验 value,可能删掉别人后来获得的锁。
  • 把 Lua 的作用说成提升性能,没说清 GET 和 DEL 原子化才是关键。
  • 认为 Redis 锁拿到后业务就绝对安全,忽略锁过期、续期失败、主从切换和旧持有者写入。

面试官追问

为什么不能 SETNX 后再 EXPIRE?

这两个命令之间如果客户端崩溃,锁 key 可能没有过期时间,造成死锁。SET NX PX 把加锁和设置 TTL 合成一条原子命令。

锁过期时间怎么设置?

要覆盖业务正常耗时和抖动,并尽量短到故障后能快速恢复。不能准确估计时,需要续期机制、业务幂等和超时保护一起兜底。

业务没执行完锁过期了怎么办?

可以做看门狗续期,但每次续期都要校验 owner,业务结束后停止续期。更关键的是业务写入要有 fencing token 或状态校验,防止旧持有者继续写。

Redis 主从切换会带来什么锁风险?

Redis 复制通常是异步的。主节点加锁后还没同步就宕机,提升的新主可能没有锁记录,其他客户端会再次获得锁,导致并发持锁。