news 2026/10/6 2:52:31

Redis 分布式锁:一行命令抢锁,但你真的会释放吗?(附可运行源码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 分布式锁:一行命令抢锁,但你真的会释放吗?(附可运行源码)

Redis 分布式锁:一行命令抢锁,但你真的会释放吗?(附可运行源码)

作者:鱼宵 | 实战驱动系列 · 第 1 篇

完整可运行工程(pom.xml + 源码 + 运行说明)已开源在 Gitee:https://gitee.com/j67mk2/redis-journey


一、一个真实场景:3 台机器抢同一个库存

你的订单服务上了 3 台机器。用户下单,扣库存:

// 这段代码在 3 台机器上同时跑if(stock>0){stock=stock-1;// 写回数据库……}

单机时代synchronized就够了,但它是JVM 级的锁——3 台 Tomcat 就是 3 个 JVM,互相管不着。库存剩 1 件,可能卖出 3 单。

类比:多人共用一个公共卫生间,门后挂"有人/没人"的牌子。Redis 就是那块公共牌子。

锁要放到所有机器都能看到、都认账的地方 →Redis 分布式锁。


二、加锁:为什么必须是SET key value NX EX一条命令?

先看错误写法:

SETNX lockKey 1 # 第一步:抢占 EXPIRE lockKey 10 # 第二步:设过期

问题:两步之间进程一旦崩了(刚抢到锁、还没设过期就被 kill),这把锁永远没有过期时间 = 死锁。

正确写法——"不存在才设置"和"设过期"合成一条原子命令:

SET lock:product:1001 唯一标识 NX EX 10
参数作用大白话
NXNot eXists,key 不存在才设置成功牌子是"有人"就抢不到
EX 1010 秒自动过期就算持锁者崩了,锁 10 秒后自动释放,不死锁
合一条抢占+过期无中间态翻牌子 + 装定时器是同一个动作

Java 里核心就三行:

SetParamsparams=newSetParams().nx().ex(expireSeconds);Stringresult=jedis.set(lockKey,requestId,params);return"OK".equals(result)?requestId:null;// 抢到返回唯一标识,没抢到返回 null

三、value 为什么要存"唯一标识"?

防误删。场景:线程 A 拿锁,业务跑得太慢,10 秒到了锁自动过期;线程 B 抢到锁;A 终于跑完去解锁——如果它无脑DEL,删掉的是B 的锁!

所以解锁前必须确认:锁的 value 还是不是我当初拿到的 UUID?是我的我才删。

StringrequestId=UUID.randomUUID().toString();// 每个持锁者一个专属编号// 解锁时:get 出来的 value 等于我的 requestId 才允许 del

四、解锁:为什么必须用 Lua 脚本?

错误写法(先判断、再删除,两条命令):

if get(lockKey) == 我的UUID: # 第一步:判断 del(lockKey) # 第二步:删除

窗口期:判断完"是我的",正要del的瞬间,锁刚好到期自动释放、B 抢到了锁 → 你又把 B 的锁删了。

Lua 把"判断 + 删除"焊死成一个原子操作,Redis 执行整段脚本期间不插队任何命令:

ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])-- 是我的才删elsereturn0-- 不是我的,绝不动手end

至此,三板斧齐了,完整工具类:

importredis.clients.jedis.Jedis;importredis.clients.jedis.params.SetParams;importjava.util.UUID;/** * Redis 分布式锁(完整版,可直接运行) * 三板斧:SET NX EX 原子加锁 + value 存唯一标识 + Lua 原子解锁 */publicclassDistributedLock{privatefinalJedisjedis;publicDistributedLock(Jedisjedis){this.jedis=jedis;}/** * 加锁。成功返回唯一标识 requestId(解锁时必须带上);失败返回 null */publicStringlock(StringlockKey,intexpireSeconds){StringrequestId=UUID.randomUUID().toString();// nx() = 不存在才设置;ex(秒) = 过期时间。合成一条原子命令SetParamsparams=newSetParams().nx().ex(expireSeconds);Stringresult=jedis.set(lockKey,requestId,params);return"OK".equals(result)?requestId:null;}/** 解锁脚本:判断 + 删除,一个原子操作 */privatestaticfinalStringUNLOCK_LUA="if redis.call('get', KEYS[1]) == ARGV[1] then "+" return redis.call('del', KEYS[1]) "+"else "+" return 0 "+"end";/** * 释放锁。 * @return true=成功删除了自己的锁;false=锁不是自己的(或已过期),没动它 */publicbooleanunlock(StringlockKey,StringrequestId){Objectresult=jedis.eval(UNLOCK_LUA,1,lockKey,requestId);returnresult!=null&&Long.parseLong(result.toString())>=1;}}

五、完整演示:两个线程抢同一把锁

importredis.clients.jedis.Jedis;importjava.time.LocalTime;importjava.time.format.DateTimeFormatter;publicclassMain{privatestaticfinalStringHOST="127.0.0.1";privatestaticfinalintPORT=6383;// 本机 Docker 起的 redis:7 容器privatestaticfinalStringLOCK_KEY="lock:product:1001";privatestaticfinalintEXPIRE_SECONDS=10;privatestaticfinalDateTimeFormatterFMT=DateTimeFormatter.ofPattern("HH:mm:ss.SSS");publicstaticvoidmain(String[]args)throwsInterruptedException{try(Jedischeck=newJedis(HOST,PORT)){System.out.println("["+now()+"] 连接 Redis 成功,ping = "+check.ping());check.del(LOCK_KEY);// 清残留锁,保证结果干净}// ===== 线程 A:抢到锁,做 3 秒业务,再释放 =====ThreadthreadA=newThread(()->{Jedisjedis=newJedis(HOST,PORT);DistributedLocklock=newDistributedLock(jedis);StringrequestId=lock.lock(LOCK_KEY,EXPIRE_SECONDS);System.out.println("["+now()+"] 线程A:抢到锁成功!requestId = "+requestId);try{System.out.println("["+now()+"] 线程A:拿着锁做业务中(模拟 3 秒)...");Thread.sleep(3000);System.out.println("["+now()+"] 线程A:业务做完了");}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{booleanreleased=lock.unlock(LOCK_KEY,requestId);System.out.println("["+now()+"] 线程A:释放锁,结果 = "+released);jedis.close();}},"线程A");// ===== 线程 B:反复重试,直到 A 释放 =====ThreadthreadB=newThread(()->{sleepQuietly(200);// 稍晚启动,保证 A 先拿到锁Jedisjedis=newJedis(HOST,PORT);DistributedLocklock=newDistributedLock(jedis);StringrequestId=null;inttryCount=0;while(tryCount<20){tryCount++;requestId=lock.lock(LOCK_KEY,EXPIRE_SECONDS);if(requestId!=null){System.out.println("["+now()+"] 线程B:第 "+tryCount+" 次尝试 → 抢到锁了!requestId = "+requestId);break;}else{System.out.println("["+now()+"] 线程B:第 "+tryCount+" 次尝试 → 抢锁失败,0.5s 后重试");sleepQuietly(500);}}if(requestId==null){System.out.println("["+now()+"] 线程B:试了 20 次都没抢到,放弃");return;}sleepQuietly(500);booleanreleased=lock.unlock(LOCK_KEY,requestId);System.out.println("["+now()+"] 线程B:用完锁,释放锁,结果 = "+released);jedis.close();},"线程B");threadA.start();threadB.start();threadA.join();threadB.join();System.out.println("["+now()+"] 演示结束。");}privatestaticStringnow(){returnLocalTime.now().format(FMT);}privatestaticvoidsleepQuietly(longms){try{Thread.sleep(ms);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}

怎么跑

# 1. 启动 Redis(任选一种)dockerrun-d--nameredis-lock-p6383:6379 redis:7# 2. 运行 Main(IDEA 直接右键运行,或命令行)mvn compile exec:java-Dexec.mainClass=Main

实测输出(跑出来应该和这份一致)

[15:51:04.861] 连接 Redis 成功,ping = PONG [15:51:04.924] 线程A:抢到锁成功!requestId = eae4577b-b541-48f0-b446-80fb57d4db0a [15:51:04.924] 线程A:拿着锁做业务中(模拟 3 秒)... [15:51:05.092] 线程B:第 1 次尝试 → 抢锁失败,0.5s 后重试 [15:51:05.595] 线程B:第 2 次尝试 → 抢锁失败,0.5s 后重试 [15:51:06.098] 线程B:第 3 次尝试 → 抢锁失败,0.5s 后重试 [15:51:06.599] 线程B:第 4 次尝试 → 抢锁失败,0.5s 后重试 [15:51:07.112] 线程B:第 5 次尝试 → 抢锁失败,0.5s 后重试 [15:51:07.615] 线程B:第 6 次尝试 → 抢锁失败,0.5s 后重试 [15:51:07.930] 线程A:业务做完了 [15:51:07.931] 线程A:释放锁,结果 = true [15:51:08.121] 线程B:第 7 次尝试 → 抢到锁了! [15:51:08.637] 线程B:用完锁,释放锁,结果 = true [15:51:08.637] 演示结束。

请盯着这份输出问自己 3 个问题:

  1. 为什么 B 前面6 次都失败、偏偏第 7 次成功?(答案藏在时间线里)
  2. 如果我把Thread.sleep(3000)改成5000,B 会提前成功还是更晚?为什么?
  3. 如果我把EX 10改成EX 0.5(过期 0.5 秒),会发生什么恐怖的事?(提示:误删!——亲手跑一次,你会永远记住 Lua 那 5 行代码为什么存在)

跑出来的输出和我这份不一样?把环境(JDK 版本 / Redis 版本 / Docker)发评论区,我们一起查。


六、跑完之后的加分题(生产环境三件事)

1. 锁的过期时间必须 > 业务最长耗时,否则业务没跑完锁先释放,等于没锁。

2. Redisson 看门狗(watchdog):生产环境别手写,直接用 Redisson。锁快到期自动帮你续租,直到你主动释放。一句话:防"业务还没跑完,锁先被别人抢走"。

3. 可重入:同一个线程已持锁,再进一个也要加锁的方法,value 里记计数器,进来 +1、出去 -1,减到 0 才真释放。


七、面试回答模板(背下来)

面试官:Redis 分布式锁怎么实现?

三板斧:① 加锁用SET key 唯一标识 NX EX 秒数一条原子命令,NX 保证不存在才设置、EX 保证自动过期,杜绝死锁;② value 存持锁者唯一标识,防止业务超时锁自动过期后误删新持有者的锁;③ 解锁用 Lua 脚本把"判断 value + 删除"合成原子操作,消除判断到删除之间的竞态窗口。生产环境直接用 Redisson,看门狗自动续租、支持可重入。


八、总结

三板斧解决什么问题不这么做的后果
SET key value NX EX抢占 + 过期原子化分两条命令 = 死锁
value 存唯一标识防误删别人的锁无脑 DEL = 误删
Lua 判断 + 删除消除竞态窗口get 再 del = 又误删

九、关于这个系列

本文是「Java 后端实战精通营」系列第 1 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都来自真实运行,完整可运行工程在 Gitee:

👉Redis 实战精通营(10 课):https://gitee.com/j67mk2/redis-journey

本课源码位置:lesson-05/(分布式锁 + 缓存一致性,跑完本文演示后可继续做缓存一致性实验,同样可运行)

后续系列(陆续发布):Dubbo / JVM / JUC / RocketMQ / MySQL / Elasticsearch,每门课配完整可运行工程 + 高频面试 30 问 + 简历包装话术。

下一篇:《缓存一致性:先更新数据库再删缓存,为什么?延迟双删又是干嘛的?》——同样有可运行源码,提前git clone等着。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 2:51:52

一点小碎片——每个线程独有的东西——上下文+栈

修正一下&#xff1a;这里是TCB。但是由于一个进程内所有线程共享虚拟地址&#xff0c;这些所谓“独有”的资源是可以被同进程另一个线程访问到的。不过只要指针不瞎指&#xff0c;只有对应线程知道自己的栈空间。这个“独有”就像只纸老虎&#xff0c;原则上你是害怕他&#x…

作者头像 李华
网站建设 2026/10/6 2:50:51

samba共享匿名访问,用户访问,账户映射,访问地址限制方法

Samba 是linux中的一个共享服务&#xff0c;可以让linux&#xff0c;windows 互相共享&#xff0c;今天来搭建一下。这里用的镜像里的包。rpm -ivh samba-3.6.9-164.el6.x86_64 # 下载sambavim /etc/samba/smb.conf #配置文件先搞一个匿名的访问。下面是要修改的参数。…

作者头像 李华
网站建设 2026/10/6 2:50:20

page_alloc zone_names

zone_names[]——每个 zone 类型对应的人类可读名称字符串数组。它是 zone 的"名字表"&#xff0c;用于打印、调试、/proc 输出等。mcrjnjfbr一、作用与类型char * const zone_names[MAX_NR_ZONES] { ... };类型&#xff1a;char * const [MAX_NR_ZONES]char *&…

作者头像 李华
网站建设 2026/10/6 2:49:17

基于YOLO姿态估计和XGBoost的零售店铺盗窃行为实时检测系统

摘要&#xff1a;随着零售业的快速发展&#xff0c;商品失窃问题日益严重&#xff0c;传统的视频监控系统依赖人工监控&#xff0c;效率低下且成本高昂。针对上述问题&#xff0c;本文提出了一种基于深度学习的智能盗窃行为检测系统&#xff0c;以实现对零售环境中可疑行为的自…

作者头像 李华
网站建设 2026/10/6 2:49:16

OpenCV 在 C# 中的“低代码开发”,核心思路是把常用的视觉算法封装成可视化工作流节点,在图形界面里拖拽连线完成算法编排,然后一键导出 C# 代码调用

OpenCV 在 C# 中的“低代码开发”&#xff0c;核心思路是把常用的视觉算法封装成可视化工作流节点&#xff0c;在图形界面里拖拽连线完成算法编排&#xff0c;然后一键导出 C# 代码调用。它解决的是传统 OpenCvSharp 开发中“每个算子都要手写代码、调参靠反复编译”的效率问题…

作者头像 李华
网站建设 2026/10/6 2:49:02

组网第六课:物理层到底在干嘛——从比特到信号的魔法

组网第六课&#xff1a;物理层到底在干嘛——从比特到信号的魔法 文章目录组网第六课&#xff1a;物理层到底在干嘛——从比特到信号的魔法一、物理层到底负责什么&#xff1f;二、常见的介质与编码从编码到接口&#xff1a;信号在介质上的旅程三、介质三兄弟&#xff1a;铜、光…

作者头像 李华