Lazy loaded image
Redis01丨面试官问 Redis 分布式锁,第一句话还在说 SETNX?那基本危险了
字数 4920阅读时长 13 分钟
2026-5-4
type
Post
status
Published
date
May 4, 2026
slug
summary
tags
技术探索
category
icon
password
面试官问你:
Redis 怎么实现分布式锁?
很多人的第一反应是:
SETNX lock value EXPIRE lock 30
先用 SETNX 抢锁,抢到了之后再设置过期时间。
看起来没问题,对吧?
但如果你在面试里这么答,面试官大概率会继续追问:
如果 SETNX 成功了,但是还没来得及执行 EXPIRE,服务突然宕机了怎么办?
这个问题一出来,如果答不上来,就危险了。
Redis 分布式锁考的是你能不能回答清楚几个核心问题:
第一,加锁怎么避免死锁? 第二,解锁怎么避免误删别人的锁? 第三,业务没执行完,锁提前过期怎么办? 第四,Redis 自己出问题了怎么办? 第五,锁失效以后,业务层有没有兜底?
很多人以为 Redis 分布式锁就是“往 Redis 里放一个 key”。
但面试官真正想听的是:你是否理解原子性、异常恢复、并发安全、自动续期和分布式故障边界
这篇文章就从最容易踩坑的 SETNX 开始,一步一步把 Redis 分布式锁讲清楚。

为什么需要分布式锁?

先从最简单的场景说起。
以前应用只部署在一台机器上,多个线程抢同一个资源,用 Java 本地锁就够了。
比如:
因为这些线程都在同一个 JVM 里,它们看到的是同一把锁。
但是现在很多服务都是分布式部署的。
同一个服务可能部署了 5 台机器,甚至几十个实例。
比如秒杀扣库存:
服务实例 A 要扣库存 服务实例 B 也要扣库存 服务实例 C 也要扣库存
这几个服务实例不在同一个 JVM 里。
你在服务实例 A 里加一个 synchronized,只能锁住 A 这台机器里的线程,根本锁不住 B 和 C。
所以我们需要一个所有服务实例都能访问的公共协调者。
Redis 就经常被用来做这个协调者。
核心思路很简单:
谁能在 Redis 里成功创建这把锁,谁就获得执行资格。 谁创建失败,谁就等待或者重试。
但问题来了:
这把锁到底应该怎么创建?

错误方案:SETNX 后再 EXPIRE

很多人的答案是:
意思是:
先用 SETNX 抢锁。
抢到了,再给锁设置 30 秒过期时间。
SETNX 的语义是:
如果 key 不存在,就设置成功。 如果 key 已经存在,就设置失败。
这不就像一把锁吗?
问题就在这里。
假设线程 A 执行了:
Redis 返回成功。
但是 A 还没来得及执行:
服务突然宕机了。
这时候 Redis 里就留下了一个没有过期时间的锁
后面线程 B、线程 C、线程 D 再来抢锁,全部失败。
因为这个锁永远不会释放。
这就是死锁。
【配图 1:SETNX 后还没 EXPIRE 就宕机,导致锁永不过期】
问题的根源在于:
加锁 和 设置过期时间 是两个独立操作。
只要这两个操作之间发生宕机、超时、网络异常,就可能出现:
锁加上了,但是没有过期时间。
所以SETNX + EXPIRE 不能作为最终答案。

正确加锁:SET NX PX 一条命令完成

正确做法是使用 Redis 的扩展 SET 命令,把“加锁”和“设置过期时间”合成一条命令:
拆开看:
SET Redis 设置 key 的命令 lock:order:1001 锁的 key 9f3c2a8e-uuid 当前请求的唯一标识 NX key 不存在才设置 PX 30000 30 秒后自动过期,单位是毫秒
也可以理解成:
如果 lock:order:1001 不存在, 就把它的 value 设置成 9f3c2a8e-uuid, 并且让它 30 秒后自动过期。
这条命令的关键价值是:
加锁 和 设置过期时间 是一个原子操作。
要么同时成功。
要么同时失败。
不会出现“锁加上了,但是没有过期时间”的中间状态。
这里面试官可能会追问一句:
为什么 SET NX PX 是原子的?
可以这样回答:
因为它是 Redis 的单条命令。单条 Redis 命令执行期间不会被其他命令插入,所以 SET key value NX PX 可以保证加锁和设置过期时间作为一个整体完成。
到这里,我们解决了第一个问题:
如何避免死锁? 答案:用 SET key value NX PX expireTime。
但是,事情还没结束。
因为锁有了过期时间以后,又会引出第二个坑:
锁提前过期后,线程会不会误删别人的锁?

第二个坑:业务没执行完,锁提前过期了

假设现在有这么一个场景。
线程 A 加锁成功:
锁的过期时间是 10 秒。
但是线程 A 的业务逻辑比较慢,执行了 15 秒。
于是时间线变成这样:
第 0 秒:线程 A 加锁成功,锁 10 秒后过期 第 10 秒:线程 A 还没执行完,Redis 自动释放锁 第 11 秒:线程 B 加锁成功 第 15 秒:线程 A 执行完业务,开始释放锁
如果线程 A 在最后直接执行:
那就出问题了。
因为第 15 秒的时候,Redis 里的锁已经不是线程 A 的锁了。
它已经是线程 B 的锁了。
线程 A 这一删,相当于把线程 B 的锁删掉了。
这就是误删别人的锁。
【配图 2:锁提前过期后,线程 A 误删线程 B 的锁】
这个问题比死锁更隐蔽。
因为很多代码都会这样写:
看起来很规范。
但如果锁已经过期,并且被其他线程重新获取了,这个 finally 里的删除操作就会破坏互斥性。
所以,释放锁不能简单地 DEL

五、正确解锁:只能删除自己的锁

怎么避免误删别人的锁?
答案是:
加锁时写入唯一标识。 解锁时先判断锁是不是自己的。 如果是自己的,才能删除。 如果不是自己的,不能删除。
比如线程 A 加锁时写入:
线程 B 加锁时写入:
这里的 request-id-Arequest-id-B 不能写死。
它应该是当前请求、当前线程、当前业务操作的唯一标识,比如 UUID。
解锁时,不能直接删除 key,而是要先判断:
Redis 里当前锁的 value,是否等于自己的 requestId?
如果相等,说明这把锁还是自己的,可以删除。
如果不相等,说明这把锁已经过期,并且被别人重新拿到了,不能删除。
很多人会写出这样的伪代码:
这个思路方向是对的。
但代码仍然不安全。
因为 GETDEL 是两个操作。
中间仍然可能被其他客户端插队。
比如:
线程 A 执行 GET,发现 value 是自己的。 就在这个瞬间,锁过期了。 线程 B 加锁成功。 线程 A 继续执行 DEL,把 B 的锁删了。
所以,判断和删除也必须保证原子性。
在 Redis 里,通常用 Lua 脚本来完成:
这段 Lua 脚本做了两件事:
第一步,判断锁的 value 是否等于自己的 requestId。 第二步,如果是自己的锁,才执行 DEL。
【配图 3:正确解锁,先判断 value,再删除】
这里真正关键的是:
把 判断 value 和 删除 key 放到 Redis 服务端一次性执行。
这样中间就不会被其他客户端插队。
到这里,我们解决了第二个问题:
如何避免误删别人的锁? 答案:唯一 value + Lua 脚本解锁。

六、第三个问题:锁过期时间到底设置多久?

现在你可能会想:
既然锁可能提前过期,那我把过期时间设置长一点不就行了吗?
比如设置成 5 分钟,甚至 10 分钟。
这样业务再慢,也不容易超过锁的过期时间。
这个思路有一定道理,但它又会带来另一个问题。
如果持锁线程真的宕机了,锁就要等很久才能自动释放。
这段时间内,其他线程都拿不到锁。
所以锁的过期时间太短不行,太长也不行。
这就是分布式锁里很经典的矛盾:
锁时间太短: 业务还没执行完,锁提前释放,可能导致并发执行。 锁时间太长: 持锁线程宕机后,其他线程要等很久才能继续执行。
那有没有办法让锁的过期时间既不要太短,也不要太长?
有。
这就是自动续期机制。
也就是面试里经常提到的:
Watchdog。

七、Watchdog:业务没结束,就自动续期

Watchdog 可以理解成一个后台守护线程。
当线程 A 加锁成功后,Watchdog 会定期检查:
这把锁是不是还属于线程 A? 线程 A 的业务是不是还没执行完?
如果是,它就帮线程 A 延长锁的过期时间。
比如锁默认 30 秒过期。
Watchdog 每隔一段时间检查一次。
如果业务还没结束,就把锁的 TTL 重新续到 30 秒。
这样就避免了业务没执行完,锁却提前释放的问题。
【配图 4:Watchdog 自动续期机制】
Java 后端项目里,很多人会直接使用 Redisson 的 RLock
这段代码里,lock.lock() 默认会使用 Watchdog 自动续期。
但是这里有一个细节很容易被忽略:
如果你显式传入了 leaseTime,意思是告诉 Redisson:
这把锁 10 秒后自动释放。
这种情况下,一般就不会走默认的 Watchdog 自动续期机制。
Redisson 的 Watchdog 主要在没有显式指定 leaseTime 时生效。如果指定了固定租约时间,就要自己评估业务是否可能超过这个时间。
不过,Watchdog 也不是万能的。
它主要解决的是:业务正常执行,但是执行时间不确定。
它不能解决所有分布式故障。
比如:
JVM 长时间 STW 客户端长时间假死 网络分区 Redis 节点故障 业务线程卡死 显式指定 leaseTime 后锁不会自动续期
 
所以,Redisson 的 Watchdog 在工程上封装了自动续期能力,可以降低业务执行时间不确定导致的锁提前过期问题,但它不是绝对可靠的分布式一致性方案。遇到 Redis 故障、网络分区、客户端长时间停顿等情况,仍然需要业务层兜底

到这里,只是解决了“怎么正确使用 Redis 锁”

前面讲的内容,其实主要解决的是:
客户端怎么正确使用 Redis 锁。
比如:
加锁要用 SET NX PX。 解锁要用唯一 value + Lua。 业务时间不确定要考虑 Watchdog。
这些属于“使用姿势”的问题。
但分布式锁还有第二层问题:
Redis 自己并不是绝对可靠的协调中心。
也就是说,就算你的客户端代码写对了,Redis 自己发生故障时,锁仍然可能失效。

Redis 主从切换后,锁还安全吗?

如果你只用一个 Redis 实例来做分布式锁,那么这个 Redis 实例本身就是单点。
有人可能会说:
那我用 Redis 主从不就行了吗?
也不完全行。
因为 Redis 主从复制通常是异步的。
可能出现这种情况:
线程 A 在 Redis 主节点加锁成功。 这把锁还没来得及同步到从节点。 主节点突然宕机。 从节点升级为新的主节点。 线程 B 在新的主节点上加锁成功。
这时候就可能出现一个严重问题:
线程 A 以为自己拿到了锁。 线程 B 也以为自己拿到了锁。
也就是说,系统里同时出现了两个持锁者。
这直接破坏了分布式锁最核心的互斥性。
【配图 5:Redis 主从切换导致锁丢失】
所以,Redis 单节点锁不是绝对安全的。
这时候就要区分一个非常重要的概念:
你用这把锁,是为了提升效率,还是为了保证正确性?

效率型锁 vs 正确性锁

Redis 分布式锁最容易被误用的地方,就是大家把所有“加锁场景”都混在一起。
但真实工程里,锁可以分成两类:
类型
目标
Redis 锁是否适合
典型场景
效率型锁
减少重复执行,降低资源浪费
比较适合
缓存重建、定时任务抢占、防重复提交
正确性锁
保证数据绝对正确
不能只靠 Redis 锁
资金变更、库存扣减、订单状态流转
什么是效率型锁?
比如缓存击穿时,只希望一个线程去重建缓存,其他线程等待。
即使极端情况下有两个线程同时重建了缓存,最多是多查了一次数据库,或者浪费了一点资源。
这种场景用 Redis 锁通常是可以接受的。
什么是正确性锁?
比如:
账户扣款 库存扣减 订单状态流转 优惠券核销
这些场景一旦锁失效,可能导致:
余额扣错 库存超卖 订单重复支付 优惠券重复使用
这类问题不能只靠 Redis 锁兜底。
因为 Redis 锁本身存在过期、主从切换、网络分区、客户端停顿等边界。
所以更稳的设计是:
Redis 锁负责降低并发冲突概率。 数据库约束、幂等、状态机、版本号负责保证最终正确性。
比如:
订单状态更新时,要校验状态只能从「待支付」流转到「已支付」。 库存扣减时,要校验库存不能扣成负数。 重复请求进入时,要通过幂等表或唯一索引拦截。 资源写入时,可以用版本号或 Fencing Token 防止旧请求覆盖新请求。
Redis 锁可以提高并发控制效率,但不要把系统正确性完全押在 Redis 锁上。

Fencing Token 是什么?

前面提到了 Fencing Token。
其实它可以简单理解成:
一个递增的版本号。
每次客户端成功获取锁时,系统给它发一个更大的 token。
资源层只接受更大的 token,拒绝旧 token。
举个例子。
客户端 A 获取锁,拿到 token = 10。 客户端 A 执行业务时发生长时间卡顿。 A 的锁过期了。 客户端 B 获取锁,拿到 token = 11。 客户端 B 写数据库成功,数据库记录 last_token = 11。 过了一会儿,客户端 A 恢复了。 A 继续拿着 token = 10 去写数据库。 数据库发现: token = 10 小于当前 last_token = 11。 于是拒绝 A 的旧请求。
这样即使客户端 A 因为锁过期后又恢复执行,也不能覆盖客户端 B 的新结果。
这就是 Fencing Token 的核心价值:
锁负责抢执行权。 token 负责防止旧持锁者恢复后继续写资源。
如果 Redis 锁过期后,旧线程继续执行怎么办?
单靠 Redis 锁无法彻底解决旧请求恢复执行的问题。强一致场景可以在资源层引入 Fencing Token 或版本号校验,让资源只接受更新的 token,拒绝旧 token 的写入。

Redlock 是不是最终答案?

Redis 官方提出过 Redlock 思路。
它的大致思想是:
准备多个相互独立的 Redis 节点。 客户端分别向这些节点尝试加锁。 只有超过半数节点加锁成功,才认为加锁成功。
比如有 5 个 Redis 节点。
客户端至少要在 3 个节点上加锁成功,才算真正拿到锁。
这样可以降低单个 Redis 节点故障带来的风险。
但是在真实工程里,也不能一句:
用 Redlock 就行。
然后结束。
因为 Redlock 本身也有使用成本和争议。
它的争议点主要在于:
在极端网络延迟、时钟漂移、客户端长时间暂停等情况下, 它是否能提供严格意义上的互斥保证。
所以,更成熟的回答不是“所有场景都上 Redlock”,而是看业务的一致性要求。
如果只是效率型互斥,比如缓存重建、防重复任务执行,单 Redis 锁通常就够用。
如果锁失效会导致严重数据错误,就不能只依赖 Redis 锁,需要在资源层增加数据库约束、幂等控制、版本号、Fencing Token,或者考虑 ZooKeeper、etcd 这类更强一致的协调组件。
这就是工程取舍。

面试时怎么回答 Redis 分布式锁?

如果面试官问:
Redis 怎么实现分布式锁?
你可以这样回答:
Redis 分布式锁不能简单用 SETNX 后再 EXPIRE,因为这两个操作不是原子的。如果 SETNX 成功后,服务还没来得及设置过期时间就宕机,就会留下一个没有 TTL 的锁,导致其他线程永远拿不到锁。
正确的加锁方式是使用 SET key value NX PX expireTime,把创建锁和设置过期时间合并成一个原子操作。其中 value 不能写死,应该使用当前请求或当前线程的唯一标识,比如 UUID。
解锁时也不能直接 DEL,因为如果业务执行时间超过锁的 TTL,锁可能已经过期并被其他线程重新获取了。此时原线程再执行 DEL,就可能误删别人的锁。所以正确做法是使用 Lua 脚本,先判断锁的 value 是否等于自己的 requestId,如果是自己的锁再删除,保证判断和删除的原子性。
如果业务执行时间不确定,还需要考虑自动续期机制,比如 Redisson 的 Watchdog。它会在持锁线程仍然存活时,定期延长锁的 TTL,避免业务没执行完锁就提前过期。不过如果显式指定了 leaseTime,就要注意自动续期可能不会生效。
但 Redis 锁也有故障边界。比如 Redis 主从复制是异步的,主节点加锁成功但还没同步到从节点时,如果主节点宕机,从节点升主,其他线程可能再次加锁成功,导致多个客户端同时认为自己拿到了锁。
所以 Redis 锁更适合效率型互斥场景,比如缓存重建、定时任务抢占、防重复提交。如果是资金、库存、订单状态流转这类正确性场景,不能只依赖 Redis 锁,还要结合数据库唯一约束、乐观锁、状态机校验、幂等表、Fencing Token,或者考虑 Redlock、ZooKeeper、etcd 这类更强的协调方案。
这个回答基本就比较完整了。
它覆盖了五层:
第一层:SET NX PX,保证加锁原子性。 第二层:唯一 value + Lua,保证安全解锁。 第三层:Watchdog,解决业务执行时间不确定。 第四层:主从切换、Redlock,说明你知道 Redis 锁的故障边界。 第五层:数据库约束、幂等、Fencing Token,说明你知道最终正确性不能只靠锁。

总结

Redis 分布式锁不是一句 SETNX 就能讲明白的。
可靠的 Redis 分布式锁,至少要回答三个基础问题:
加锁如何避免死锁? 解锁如何避免误删? 业务超时如何自动续期?
对应的答案是:
SET key value NX PX expireTime 唯一 value + Lua 脚本删除 Watchdog 自动续期
但更重要的是继续往下想:
Redis 自己故障怎么办? 主从切换会不会丢锁? 锁失效后业务层有没有兜底?
所以,Redis 分布式锁的重点不是背命令,而是理解它的边界。
更成熟的工程思路应该是:
效率型互斥,可以用 Redis 锁提高并发控制效率。 正确性场景,不能只靠 Redis 锁,必须在资源层做最终兜底。
也就是说:
锁可以减少并发冲突。 但最终正确性,不能完全交给锁。

看到这里,有同学可能会问:
既然 Redis 分布式锁有这么多坑,那为什么不直接用 Redisson?
答案是:生产环境当然应该优先用 Redisson,而不是自己手写 Redis 锁。
Redisson 的 RLock 已经帮我们封装了 SET NX PX 加锁、Lua 安全解锁、可重入、Watchdog 自动续期、等待通知等能力。自己手写分布式锁,很容易漏掉边界情况。
但这并不代表我们可以只会一句“用 Redisson 就行”。
因为 Redisson 解决的是锁的工程实现复杂度,而不是所有分布式一致性问题。
Redisson 底层依然依赖 Redis。只要依赖 Redis,就绕不开 Redis 主从切换、网络分区、客户端长时间停顿、锁过期后旧请求继续执行这些问题。
所以更准确的工程结论是:
普通业务场景下,优先使用 Redisson,避免自己造轮子。
但在资金、库存、订单状态流转这类强一致场景里,不能只依赖 Redisson 锁。还需要在业务层和资源层增加幂等表、唯一约束、状态机校验、乐观锁版本号或者 Fencing Token,保证即使锁失效,数据也不会错。
不能知其然不知其所以然,不能只会用Redission,所以还是得了解redis分布式锁底层。
回到首页