用Redis的setnx来搞个简单分布式锁,原理和实现聊聊
- 问答
- 2025-12-31 11:31:11
- 1
用Redis的setnx命令来做一个简单的分布式锁,这个想法其实非常直观,咱们先打个比方,想象一下,有一个公共的卫生间,这个卫生间只有一个坑位,现在有好几个人(代表分布在不同机器上的程序或服务)都可能需要上厕所,分布式锁要解决的核心问题就是:确保在同一时间,只有一个人能进去把门锁上使用,其他人都得在外面等着,等里面的人出来了,下一个人才能进去。
在这个比喻里,那个唯一的坑位就相当于我们想保护的共享资源,比如一段代码、一个数据库操作或者一个文件,而Redis,在这里扮演的就是那个卫生间大门上唯一的一把锁的角色。
核心原理:SETNX 命令为什么能当锁用?
Redis的SETNX命令,全称是“SET if Not eXists”,意思是“如果不存在才设置”,这个命令的特性完美契合了锁的需求。
它的工作方式很简单:你给一个键(Key)尝试设置一个值(Value),如果这个键之前不存在,Redis会设置成功,并返回1;如果这个键已经存在了,那么这次设置操作就不会执行,并返回0。
对应到我们的锁上:
- 锁的Key:就是这把锁的名字,比如你想给“用户A的账户扣款”这个操作加锁,Key就可以叫
lock:userA_account,所有需要对用户A账户进行操作的进程,都认准这个Key来抢锁。 - 锁的Value:可以是一个随机生成的、唯一的值(比如UUID),这个值很重要,后面会讲到它的用途,主要是为了确保只有锁的持有者才能释放锁,防止误删别人的锁。
- SETNX 返回1:表示设置成功,也就是抢锁成功,恭喜你,你现在可以进入“卫生间”执行你的关键代码了。
- SETNX 返回0:表示设置失败,因为Key已经存在,这意味着锁已经被别人拿走了,你抢锁失败,需要等待或重试。
最基本的抢锁操作就是一行命令:SETNX lock:userA_account some_random_value。
基础实现和必须考虑的问题
如果只是上面那样,那这个锁也太脆弱了,一个能用在生产环境的锁,至少要解决两个致命问题:
死锁问题:万一锁的主人挂了,锁永远不释放怎么办?
想象一下,一个人进了卫生间,突然晕倒在里面了,门一直锁着,外面的人永远也进不去,在程序中,就是某个服务实例抢到锁之后,还没来得及释放锁,就因为网络问题、机器宕机或者程序BUG crash掉了。
解决方案是给锁加一个过期时间(Expire Time),就像给卫生间设置一个使用超时,比如15分钟,如果超过15分钟里面的人还没出来,系统就认为他出意外了,自动把门打开(删除锁),让下一个人有机会进去。

在Redis中,我们需要在设置锁的同时给它设置一个过期时间,这里有个关键点:设置值和设置过期时间必须是原子操作,你不能先SETNX成功,然后再执行EXPIRE命令,因为如果在这两条命令之间,你的服务宕机了,那么锁就变成了一个永不过期的键,真成死锁了。
幸运的是,Redis从2.6.12版本开始,SET命令本身已经支持了一系列扩展参数,可以原子性地完成SETNX和EXPIRE的操作,现在推荐的做法是直接使用SET命令:
SET lock:userA_account some_random_value NX PX 15000
这行命令的意思是:当键 lock:userA_account 不存在(NX)时,将其值设置为 some_random_value,并设置过期时间为15000毫秒(PX),这是一条原子命令,要么一起成功,要么一起失败。
误删别人锁的问题
现在有了过期时间,死锁问题解决了,但又来了新问题:如果锁超时释放了,但实际上锁的持有者并没有挂,只是因为它执行的业务逻辑特别复杂,超过了我们设定的15秒(这就是所谓的“锁超时”),会发生什么?

- 进程A抢到锁,开始执行耗时操作(比如30秒)。
- 15秒后,锁因为过期自动释放了。
- 进程B此时抢锁成功,也进入了关键代码区。
- 又过了5秒,进程A的耗时操作终于做完了,它依然会执行释放锁的流程(删除Key)。
- 这下坏了,进程A删除的其实是进程B刚创建的锁!进程B还在执行任务,锁没了,进程C就可能趁机也挤进来,导致数据错乱。
为了解决这个问题,我们之前设置的唯一Value就派上用场了,在释放锁的时候,我们不能无脑删除,需要先做一个判断:这把锁还是不是我当初设置的那一把?
释放锁的逻辑需要变成三步,并且要保证原子性(使用Lua脚本是Redis中实现复杂原子操作的常见方法):
- 获取锁当前的值。
- 判断这个值是否等于我当初设置的那个随机值。
- 如果相等,说明锁还是我的,可以删除;如果不相等,说明锁已经属于别人了,我不能删。
用Lua脚本实现大致如下:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
(脚本来源:基于Redis官方文档和社区普遍实践)
在执行删除操作时,客户端会把这个脚本和它自己持有的随机值 some_random_value 一起发送给Redis服务器,服务器会原子性地执行这段逻辑,从而确保安全释放。
简单总结
一个用SETNX(现在更常用带NX参数的SET命令)实现的简单分布式锁,核心流程就是:
- 加锁:使用
SET key random_value NX PX timeout原子命令争抢锁。 - 执行业务逻辑:抢锁成功后,执行受保护的关键代码。
- 解锁:使用Lua脚本,先比对锁的Value是否匹配,匹配则删除锁。
这种锁实现起来简单,能满足很多不太复杂的场景,但它并不是完美的,比如它不具备可重入性(同一个线程多次获取同一把锁),在高并发场景下可能遇到锁超时导致的多客户端同时进入临界区的问题(Redlock算法试图解决这个问题,但更复杂),作为理解分布式锁的起点,这个基于SETNX的思路是非常经典和实用的。
本文由盘雅霜于2025-12-31发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/71857.html
