用Setnx搞定Redis分布式锁,感觉挺简单但细节还得注意点啥
- 问答
- 2025-12-31 20:50:53
- 5
用Setnx实现Redis分布式锁,表面看就是一句命令的事,但真想把它用在生产环境,里头门里门道可多了,稍不注意就会掉坑里,这就像你以为只是用一把普通的锁,结果发现这把锁自己会消失,或者干脆把别人给锁里头了,咱们就顺着这个思路,把这些需要注意的细节一个个捋清楚。
第一,最核心的坑:锁怎么释放? 这是最基本也是最容易出问题的地方,你用了 SETNX key value 命令,成功设置了键,就表示拿到锁了,但处理完业务逻辑后,你必须得记得用 DEL 命令把这个键删掉,释放锁给别人用,这听起来是废话,但在复杂的代码流程里,万一某个地方提前返回了,或者抛了异常,导致 DEL 命令没执行,那这个锁就永远成了“死锁”,其他进程再也别想拿到,系统就卡死了,第一个要点是:释放锁的操作必须放在 finally 代码块里执行,确保无论业务逻辑成功还是失败,锁最终都能被释放,这是最基本的保障。
第二,锁的归属问题:你只能释放自己的锁。 想象一个场景:进程A拿到了锁,但是业务逻辑执行的时间有点长,超过了我们设定的锁过期时间,这时,Redis自动把锁给删了(这是为了解决上面死锁问题引入的机制,后面会细说),锁一没,进程B立马就拿到了锁,过了一会儿,进程A终于执行完了,它顺手就执行了 DEL 命令,坏事了,进程A删除的其实是进程B刚创建的锁!进程B还在兢兢业业地干活呢,锁没了,进程C又能进来捣乱了,这就完全失去了锁的意义。
不能简单粗暴地直接删key,解决办法是,在设置锁的时候,给每个客户端生成一个唯一的值(比如UUID)作为value,删除锁的时候,先判断一下当前锁的value是不是自己设置的那个UUID,如果是,才能删除,这个“判断值相等再删除”的操作必须是原子性的,不能分两步做(先GET再DEL),因为中间可能又被其他进程插足了,在Redis里,我们可以用Lua脚本来实现这个原子操作,简单来说就是:锁要带标识,删前要核对,操作要原子。
第三,锁的过期时间:设置多长才合适? 既然为了防止死锁我们给锁加上了过期时间(比如10秒),那下一个问题来了,这个10秒设得合不合理?如果业务逻辑非常复杂,10秒根本执行不完,那就会出现上面说的“锁提前释放”的悲剧,进程A的锁没了,进程B进来,两个人同时操作共享资源,数据就乱套了,但如果把时间设得很长,比如1分钟,万一真的发生死锁(比如服务器宕机),系统需要最多1分钟才能自动恢复。
设置过期时间是个权衡,它应该远大于业务逻辑正常执行的平均时间,留出充足的余量,这引出了一个更高级的话题:“锁续期”(Watchdog机制),就是说,另起一个线程,在业务逻辑执行期间,定时去检查锁还在不在,如果在,就自动延长一下过期时间,这样既能避免锁提前释放,又能避免长时间死锁,但这又增加了系统的复杂性。
第四,原子性操作:加锁和设置过期时间必须一起完成。 早期常见的错误做法是,先 SETNX,然后再用 EXPIRE 命令设置过期时间,这两个命令是分开的,如果在这两条命令之间,你的Redis客户端进程崩溃了,导致 EXPIRE 没执行成,那这个锁又变成永久的死锁了。加锁和设置过期时间必须是一个原子操作,幸运的是,Redis 2.6.12之后,SET 命令增加了一系列参数,可以直接用 SET key value NX PX 10000 这样的命令,一步到位地实现“不存在时设置”和“设置毫秒级过期时间”,完美解决了这个问题,现在都应该用这个命令来代替旧的 SETNX+EXPIRE 组合。
第五,要不要用Redisson这样的现成库? 当你把上面这些问题——异常处理、锁标识、原子操作、锁续期——都考虑周全后,会发现自己写出来的分布式锁代码已经相当复杂了,而在Java世界里,像Redisson这样的客户端库,已经把这些细节都封装好了,它提供了现成的、可靠的分布式锁实现,包括自动续期、可重入等高级功能,对于大多数应用场景,直接使用成熟的开源库是更稳妥、更高效的选择,能避免重复造轮子,也减少了出错的可能,除非有非常特殊的定制化需求,否则不建议自己从头实现。
用Setnx搞分布式锁,感觉简单是因为只看到了冰山一角,水面下的细节包括:确保锁必然被释放、只能释放自己的锁、合理设置并管理锁的存活时间、所有关键操作必须是原子的,把这些都想明白了,你才能真正驾驭这把看似简单的“锁”。(根据开发者社区常见技术讨论及Redis官方文档最佳实践综合阐述)

本文由盈壮于2025-12-31发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/72062.html
