数据库与缓存11 分钟阅读更新于 2026-07-30

Redis 分布式锁:SET NX PX、唯一值和安全释放

Redis 实现分布式锁的内容,讲清 SETNX 思路、SET NX PX 写法、unique_value 的作用、过期时间和安全解锁流程。

相关工具

分布式锁先解决一个很小的问题

分布式锁听起来很重,其实它先解决的是一个很具体的问题:同一时间只能让一个客户端做某件事。比如同一个订单不能被两个服务同时取消,同一个定时任务不能被多台机器重复执行,同一个库存扣减逻辑不能同时跑出两套结果。单机程序里可以用本地锁,服务一旦变成多实例,本地锁就管不住其他机器了。

相关基础内容 里给出的 Redis 分布式锁实现思路很简洁:使用 SETNX 命令,只有插入的 key 不存在才插入;如果 SETNX 的 key 已经存在,就插入失败。key 插入成功代表加锁成功,否则加锁失败。解锁过程就是删除 key,但要保证执行删除操作的客户端就是加锁的客户端,所以加锁时要设置 unique_value,解锁时先判断 unique_value 是否属于当前客户端,是才删除 lock_key。还要给锁设置过期时间,避免客户端拿到锁后异常退出,导致锁一直无法释放。

这段话已经把 Redis 分布式锁的主干讲完了:不存在才写入、写入成功算拿到锁、释放前要验身份、锁必须有过期时间。后面的所有细节,基本都是围绕这四句话展开。

Redis 分布式锁图解,展示客户端使用 SET lock_key unique_value NX PX 10000 加锁,key 不存在则加锁成功,key 已存在则失败,执行业务后先校验 unique_value 再删除锁,并提醒不要直接 DEL 和不要忘记过期时间
Redis 分布式锁的安全加锁和释放

SET lock_key unique_value NX PX 10000 同时完成不存在才写入和设置过期时间;释放锁时先比较 unique_value,再删除 lock_key。

SETNX 的核心是“没有才写”

SETNX 可以拆成 SET if Not eXists。它的意思是:只有 key 不存在时才写入成功。如果多个客户端同时对同一个 lock_key 执行 SETNX,Redis 最终只会让一个客户端写进去。写进去的那个客户端拿到锁,其他客户端失败。

这正好符合锁的语义。锁不是保存业务数据,而是保存一段占用状态。lock_key 存在,说明当前有人正在处理;lock_key 不存在,说明可以尝试进入临界区。Redis 单线程顺序执行命令,SETNX 这个判断和写入过程不会被其他命令从中间打断,因此能作为加锁的基础动作。

不过,只用 SETNX 还不够。早期一些写法会先 SETNX,再单独 EXPIRE 设置过期时间。问题是这两个动作之间如果客户端崩了,锁就已经写进去,却没有过期时间。后面其他客户端会一直拿不到锁。这就是为什么后来的推荐写法会把条件写入和过期时间放进同一条 SET 命令里。

SET NX PX 把两个动作合在一起

文中给出的命令是:SET lock_key unique_value NX PX 10000。这里的 SET 不是普通赋值,而是带参数的原子加锁命令。NX 表示 key 不存在时才写入;PX 10000 表示过期时间是 10000 毫秒,也就是 10 秒。

这个写法比 SETNX 后再 EXPIRE 更稳,因为判断 key 是否存在、写入 unique_value、设置过期时间是在一条命令里完成的。命令成功,锁有值,也有过期时间;命令失败,说明锁已经被别人持有。中间不会出现“锁写进去了但过期时间没设上”的窗口。

过期时间不是装饰。分布式系统里,客户端可能崩溃,网络可能断开,业务代码可能卡住。如果没有过期时间,一个异常客户端拿到锁后不释放,后面的请求就会一直被挡住。PX 或 EX 的作用,就是给锁一个自动兜底释放的时间。

unique_value 是锁的身份证

很多人写 Redis 锁时只记得 lock_key,却忽略 unique_value。资料 特别强调,加锁时要设置 unique_value,解锁时要先判断锁的 unique_value 是否为加锁客户端,是才删除 lock_key。这个细节非常重要。

为什么不能直接 DEL lock_key?假设客户端 A 拿到锁,业务执行时间超过了锁的过期时间,Redis 自动把锁删掉了。随后客户端 B 成功拿到同一个 lock_key。这个时候 A 的业务终于执行完了,如果它直接 DEL lock_key,就会把 B 正在持有的锁删掉。B 以为自己还在临界区里,实际上锁已经没了,其他客户端也可能进来。

unique_value 就是为了解决这个误删问题。每个客户端加锁时写入一个唯一值,可以是 UUID,也可以是请求 id、实例 id 加随机串。释放锁时先读取 lock_key 的值,确认它仍然等于自己当初写入的 unique_value,再删除。值不一致,说明锁已经不是自己的,不能删。

释放锁要把比较和删除放在一起

只说“先判断 unique_value,再删除 key”还差一步。判断和删除也应该是一个不可被打断的整体。否则会出现另一个窗口:客户端 A 读到锁值确实是自己的,准备删除;刚好这时锁过期,客户端 B 又加锁成功;A 随后执行 DEL,又删掉了 B 的锁。

实际写法里通常会用 Lua 脚本把比较和删除合在一起执行。脚本逻辑很短:如果 GET lock_key 的结果等于 unique_value,就 DEL lock_key;否则返回 0。Redis 执行 Lua 脚本时会把这段逻辑作为一个整体执行,不会在比较和删除之间插入其他客户端命令。

资料 原文没有展开 Lua,但它说的“解锁的时候,要先判断锁的 unique_value 是否为加锁客户端,是才将 lock_key 键删除”,落到工程上就应该这么做。分布式锁最怕的不是加不上锁,而是误删别人的锁。释放锁比加锁更需要小心。

过期时间要按业务耗时设置

PX 10000 里的 10000 只是例子,不是所有锁都固定 10 秒。过期时间太短,业务还没做完锁就过期,其他客户端会进入临界区,造成并发执行。过期时间太长,客户端异常退出后,其他请求要等很久才能重新获得锁。

设置过期时间时,至少要看业务正常执行耗时、最慢情况下的耗时、下游接口是否可能卡住、失败后是否允许重试。对很短的任务,过期时间可以短一点;对可能耗时较长的任务,要么给足超时时间,要么设计续期机制。但续期机制本身也会增加复杂度,不能随便加。

更稳妥的习惯是让临界区尽量短。拿到锁后只做必须串行的那部分逻辑,不要把大量外部调用、慢查询、文件处理都塞进锁里。锁持有时间越长,超时设置越难,系统吞吐也越容易下降。

加锁失败后不要一味死等

当 SET lock_key unique_value NX PX 10000 返回失败时,说明锁已经被其他客户端持有。这个时候应用有几种选择:直接返回稍后重试,等待一小段时间后重试,或者进入带退避的重试流程。具体选哪种,要看业务能不能等待。

比如后台定时任务抢锁,失败就退出通常没问题,下一轮调度还会再试。用户请求里的关键操作,如果可以提示“处理中”,也不一定要堵住线程一直等。只有那些必须立即完成、且等待时间可控的场景,才适合短暂重试。

重试也要有上限。很多客户端同时抢锁时,如果大家都高频重试,Redis 会被无意义请求打满。可以加入随机退避,让失败客户端隔一段不固定的时间再试。分布式锁保护的是临界区,不应该把锁外面的等待队列变成新的压力源。

Redis 锁适合什么,不适合什么

Redis 分布式锁适合控制短时间、低成本、可重试的并发动作。比如防止定时任务重复执行,限制某个资源同一时间只被一个服务刷新,避免同一用户的某个操作被并发提交多次。这些场景的共同点是:锁持有时间短,失败可以重试,业务有补偿空间。

它不适合承担特别强的金融级一致性语义。比如一旦锁判断出错就会造成不可接受的资金损失,这类场景不能只靠一个 Redis key 扛住。更稳的做法通常要结合数据库事务、唯一约束、乐观锁、幂等表、消息日志等机制,让最终事实有更强的校验。

这不是说 Redis 锁不可靠,而是要把它放在合适的位置。Redis 锁解决的是分布式服务之间的轻量互斥问题。真正的业务正确性,往往还需要数据库层和业务幂等一起兜底。

常见错误写法

第一种错误是只 SETNX,不设置过期时间。客户端异常后锁永远留在 Redis 里,后续请求一直失败。文中明确说要给锁设置过期时间,就是为了避免客户端拿到锁后异常导致锁无法释放。

第二种错误是直接 DEL lock_key。只要锁可能过期、业务可能超时,就有误删其他客户端锁的风险。正确做法是写入 unique_value,释放前先比较,确认锁仍然属于自己。

第三种错误是锁粒度太大。一个全局 lock_key 把很多互不相关的业务都串行化,会让系统吞吐掉得很厉害。锁应该围绕具体资源设计,比如按订单 id、用户 id、商品 id 拼接 key,让互不影响的请求可以并行执行。

面试可以这样回答

如果被问 Redis 如何实现分布式锁,可以先说核心命令:SET lock_key unique_value NX PX 10000。NX 表示 key 不存在才写入,写入成功代表加锁成功;PX 表示设置毫秒级过期时间,避免客户端异常后锁一直不释放;unique_value 是当前客户端生成的唯一标识,用来区分锁的持有者。

然后讲释放锁:不能直接 DEL。释放前要先判断 lock_key 对应的 value 是否等于自己加锁时写入的 unique_value,只有一致才删除。为了避免比较和删除之间发生并发变化,通常会用 Lua 脚本把 GET 判断和 DEL 删除放在一起执行。

最后补充使用边界:Redis 分布式锁适合短临界区、可重试的并发控制。过期时间要结合业务耗时设置,加锁失败要有重试或降级策略;对强一致要求很高的核心业务,还要配合数据库事务、唯一约束和幂等设计。

常见问题

Redis 分布式锁常用命令是什么?

常用写法是 SET lock_key unique_value NX PX 10000。NX 保证 key 不存在才写入,PX 设置过期时间,unique_value 标识当前加锁客户端。

为什么 Redis 锁要设置过期时间?

为了防止客户端拿到锁后异常退出,导致锁永远不释放。过期时间可以让锁在超时后自动释放。

为什么解锁前要比较 unique_value?

因为锁可能已经过期并被其他客户端重新获得。直接 DEL 可能误删别人的锁,比较 unique_value 可以确认锁仍然属于当前客户端。

释放 Redis 锁为什么常用 Lua 脚本?

因为比较 value 和删除 key 要作为一个整体执行。Lua 脚本能避免比较后、删除前锁状态被其他客户端改变。

MySQL 与 Redis

继续阅读

返回专题