Redis锁怎么弄才靠谱,设置过程和注意点分享给你
- 问答
- 2025-12-30 09:19:06
- 2
我们要明白为什么需要Redis锁,想象一下一个场景:多个用户同时抢购最后一件商品,或者多个系统同时要修改同一个数据,如果没有锁的控制,很可能会出现商品超卖(卖出去的数量比库存多)或者数据被改得乱七八糟的情况,Redis锁,本质上就是利用Redis这个速度快、性能高的内存数据库,来做一个“信号灯”,告诉其他程序:“这个资源我正在用,你们得排队等着”。
设置一个简单粗暴的锁很容易,要让它“靠谱”就需要考虑很多细节,下面我就把设置过程和关键的注意点分享给你。
最基础的锁:SETNX命令
最早大家用的是SETNX命令,意思是“SET if Not eXists”(如果不存在才设置),你可以把它想象成一个房间的钥匙只有一把,第一个来的人用SETNX在Redis里创建了一个键(比如lock:order_123),相当于拿到了钥匙进屋了,后来的人再执行SETNX,因为键已经存在,就会失败,只能在外面等着。
等屋里的人办完事,他需要调用DEL命令把这个键删掉,相当于把钥匙还回去,这样外面等待的人才有机会拿到钥匙。
基础锁的问题
但这个基础锁有巨大的缺陷:
- 死锁问题:如果拿到锁的程序(比如一个Java应用)在执行业务代码时突然崩溃了,来不及删除那个键,那么这个锁就永远存在了,其他程序会永远等待下去,这就是“死锁”。
- 误删别人锁的问题:假设程序A拿到了锁,设置超时时间是30秒,但A因为某些原因(比如进行了一次很慢的数据库查询)执行了35秒才完成,在第30秒时,锁因为超时被自动释放了,这时程序B拿到了锁,第35秒时,程序A终于执行完了,它依然会执行
DEL命令,结果把程序B刚创建的锁给删掉了!这会导致混乱。
靠谱的改进方案:SET命令扩展参数
为了解决上述问题,Redis 2.6.12版本之后,增强了SET命令的功能,使得一行命令就能完成靠谱的加锁,这才是现在推荐的做法。
设置过程如下:
SET lock_key unique_value NX PX 30000
我来解释一下这几个参数:
lock_key:锁的名称,比如lock:seckill_good_001,要根据你的业务来起名。unique_value:一个唯一的值,比如可以使用UUID生成。这个值至关重要,它是解决“误删别人锁”问题的关键。NX:表示“if Not eXists”,只有这个key不存在时才能设置成功,这保证了互斥性。PX 30000:表示这个key的过期时间是30000毫秒(即30秒),这解决了“死锁”问题,即使程序崩溃,锁也会在30秒后自动释放。
完整的加锁和解锁流程(以伪代码表示):
-
加锁:
生成一个全局唯一的UUID,client_id = "550e8400-e29b-41d4-a716-446655440000" 执行命令:SET lock:resource_name client_id NX PX 30000 如果Redis返回OK,表示加锁成功。 如果Redis返回nil,表示加锁失败,说明锁已被别人持有,这时你可以直接返回错误,或者进行重试。 -
执行业务逻辑:
在加锁成功后,执行你受保护的代码,比如扣减库存、修改订单状态等。 -
解锁:
解锁不能简单地用DEL命令,需要先判断这把锁是不是自己加的。 执行一段Lua脚本(因为Lua脚本在Redis中是原子性执行的,不会被中断): if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end 解释: KEYS[1] 就是锁的key,即 "lock:resource_name"。 ARGV[1] 就是你当时加锁时传入的唯一值 client_id。 这个脚本的意思是:先检查当前锁的值,是不是自己设置的那个UUID,如果是,才删除它(解锁);如果不是,说明锁已经过期并被其他程序获取了,那么就不做任何操作。
关键的注意点(如何更靠谱):
-
超时时间是“救生绳”也是“双刃剑”:设置过期时间(PX参数)是必须的,它是防止死锁的救生绳,但这个时间设多久是个技术活,时间太短,可能业务没执行完锁就释放了,会导致锁失效(类似上面误删锁的例子),时间太长,如果客户端真崩溃了,其他进程需要等待很久才能继续,你需要根据业务代码的平均执行时间来评估,并留出充足的余量,比如业务一般执行要2秒,你可以设置10秒或15秒。
-
唯一值(UUID)是解锁的“身份证”:一定要保证每个客户端每次加锁时使用的唯一值是不同的,这是安全解锁的基石,如果你用线程ID或者固定的字符串,很容易发生误删。
-
使用Lua脚本保证原子性:解锁时的“判断锁归属”和“删除锁”必须是原子操作,如果你用先GET再DEL的两条命令,可能在GET之后、DEL之前,锁过期了并被另一个客户端获取,你又可能误删别人的锁,Lua脚本被Redis单线程执行的特点完美解决了这个问题。
-
考虑锁的可重入性:如果一个线程在已经持有锁的情况下,再次请求这个锁,应不应该让它成功?比如在递归调用或者回调函数中又调用了加锁方法,上面的简单锁模型是不支持可重入的,如果你需要这个特性,可以在锁的value上做文章,比如value存储
client_id:thread_id:count,每次重入时count加1,解锁时减1,减到0才真正释放锁,但这会复杂很多,大部分场景可能不需要。 -
避免在单点Redis上追求极致可靠性:我们上面讨论的都是基于单个Redis实例的锁,如果这个Redis实例挂掉了,那么整个锁服务就不可用了,虽然Redis本身很稳定,但在对可靠性要求极高的金融场景,人们会使用Redlock算法(来源:Redis官方文档 Distributed locks with Redis),它需要同时向多个独立的Redis实例申请锁,多数成功才算加锁成功,这样可以容忍少数实例故障,但Redlock实现复杂,性能有损耗,而且其正确性在分布式系统领域还有争议(来源:Martin Kleppmann的文章《How to do distributed locking》),所以你要根据实际业务场景权衡是否需要这么高的可靠性。
一个靠谱的Redis锁核心就是三句话:用带NX和PX参数的SET命令加锁,锁的值要用唯一ID,用比对唯一ID的Lua脚本解锁,把握好这三点,再根据你的业务情况调整超时时间,就能应对绝大多数需要分布式锁的场景了。

本文由寇乐童于2025-12-30发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/71181.html
