简介:这份源码解析资源面向Java后端开发者与分布式系统学习者,聚焦高并发场景下Redis分布式锁的生产级落地,帮助读者理解锁的获取、续租、释放及异常处理等核心机制。压缩包共39个文件、约148KB,以14个java源码与8个class文件为主体,辅以xml、yml、properties等配置文件和gitignore规则,另有jar依赖与说明文档,目录按redis-jedis、redis-lock、redis-boot-sentinel-cluster等模块组织,便于对照阅读。已有361人学习下载。通过研读源码,读者可掌握Jedis连接Redis、Spring Boot自动化配置集成、避免死锁与活锁、保障锁性能与可靠性等实战要点,并借助模拟高并发场景的代码结构,将分布式锁理论转化为可复用的工程经验,适合希望提升系统一致性与并发处理能力的开发者参考。
1. 从一次超卖事故说起:这套 Java Redis 分布式锁源码到底能解决什么
电商大促凌晨两点,库存扣减接口突然出现超卖,排查发现两台机器上的定时任务同时抢到了同一批订单。根因不是数据库事务,而是分布式环境下本地锁失效——synchronized只能锁住单个 JVM,跨进程根本管不住。这类场景在 ERP 库存扣减、秒杀下单、高并发 IM 消息去重里反复出现,也是 Java 面试八股文里绕不开的分布式锁使用场景。
诸葛分享的这套源码,就是围绕「生产级 Redis 分布式锁」拆出来的实战工程。它用 Java + Jedis 实现锁的获取、续租、释放,配套 Spring Boot 自动配置、Sentinel 集群连接、Maven 多模块结构,一共 39 个文件,包含 8 个 Java 类、3 个 XML 配置、3 个 properties、2 个 YAML。适合已经会写 Redis 基本命令、但没在真实高并发下压过锁的后端开发者,也适合想搞懂「为什么 SETNX 不够用」的面试准备者。下面按「资源结构 → 核心实现 → 踩坑 → 进阶验证」的顺序拆开讲。
2. 工程结构与依赖拆解:多模块 Maven 里每个目录在干什么
拿到一个源码包,先别急着跑main。这套工程是典型的 Maven 多模块结构,根目录下挂着redis-lock、redis-boot-sentinel-cluster两个子模块,外加.mvn/wrapper做 Maven 版本锁定。看懂目录,才知道哪些代码是锁的核心、哪些只是环境配置。
2.1 模块划分与文件职责
从压缩包解出来的顶层结构大致是这样:
| 路径 | 类型 | 作用 |
|---|---|---|
pom.xml(根) | XML | 父 POM,统一管理依赖版本与子模块 |
redis-lock/ | 模块 | 分布式锁核心实现,锁的获取/续租/释放都在这里 |
redis-boot-sentinel-cluster/ | 模块 | Spring Boot + Sentinel 集群接入示例 |
.mvn/wrapper/ | 配置 | Maven Wrapper,保证构建版本一致 |
.settings/ | 配置 | Eclipse 工程配置(JDT、m2e 等) |
.gitignore | 规则 | 忽略 target、本地配置等不该入库的文件 |
readme.txt | 文档 | 作者留下的使用说明 |
核心逻辑集中在redis-lock模块的src/main下,test目录放单元测试。redis-boot-sentinel-cluster则是把锁接到真实 Spring Boot 环境里的样板,YAML 配置就在这个模块的src/main/resources里。很多人下载完只跑redis-lock,结果发现连不上 Sentinel,就是因为没看第二个模块的配置。
2.2 依赖与构建:pom.xml 里真正要盯的几行
父 POM 里最关键的是 Jedis 和 Spring Boot 的版本对齐。Jedis 是这套锁的底层客户端,锁命令全靠它发。构建前先确认本地 JDK 和 Maven 版本,用 Wrapper 可以省掉版本不一致的玄学问题:
# 用工程自带的 Maven Wrapper 构建,避免本地 Maven 版本差异 ./mvnw clean install -DskipTests # 如果只想编译锁核心模块 ./mvnw -pl redis-lock clean package-pl redis-lock指定只构建锁模块,-DskipTests跳过测试加快首次编译。首次构建会拉取 Jedis、Spring Boot 相关依赖,网络慢的话建议先配好镜像。构建成功后target/classes下会生成编译产物,test-classes是测试类产物。
提示:如果
./mvnw报权限错误,先执行chmod +x mvnw;Windows 下直接用mvnw.cmd。
2.3 配置文件三类格式的分工
工程里同时出现 XML、properties、YAML 三种配置,不是作者随意为之。XML 是 Maven 的pom.xml,管依赖;properties 多为 Eclipse 工程元数据和少量键值配置;YAML 则是 Spring Boot 的application.yml,管 Redis 连接、Sentinel 节点、锁超时等运行时参数。改锁行为时,优先动 YAML,别去改 properties 里的 IDE 配置,否则容易把工程元数据搞乱导致 IDE 报红。
3. 锁的核心实现:SETNX 到 Lua 释放的完整链路
这一章是整套源码的价值所在。分布式锁的难点从来不是「加锁」这一下,而是加锁之后怎么保证原子释放、怎么防误删、怎么续租。下面按锁的生命周期拆。
3.1 加锁:为什么 SETNX + EXPIRE 是错的
新手最容易写出的加锁逻辑是两条命令:先SETNX,成功后再EXPIRE。问题在于这两步不是原子的——如果SETNX成功后进程崩溃,EXPIRE没执行,这把锁就永远留在 Redis 里,变成死锁。正确做法是用一条SET key value NX PX timeout命令,把「不存在才设置」和「过期时间」合并成原子操作。
源码里对应的加锁逻辑大致是这样:
// 加锁:NX 保证互斥,PX 设置毫秒级过期,value 用唯一标识防误删 public boolean tryLock(String lockKey, String requestId, int expireTime) { // SET 命令带 NX PX 参数,一条命令完成原子加锁 String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); // "OK" 表示抢锁成功,null 表示锁已被占用 return "OK".equals(result); }lockKey是锁的键名,requestId是本次请求的唯一标识(通常用 UUID),expireTime是过期毫秒数。requestId的作用在释放锁时才体现——它标记「这把锁是谁加的」,防止 A 的锁过期后被 B 抢到,A 却把 B 的锁删了。expireTime要大于业务最长执行时间,否则业务没跑完锁就过期,互斥直接失效。
3.2 释放:Lua 脚本保证「判断 + 删除」原子
释放锁不能直接DEL,因为要先判断锁是不是自己的。如果先GET判断再DEL,两步之间锁可能刚好过期并被别人抢走,你删的就是别人的锁。源码用 Lua 脚本把判断和删除绑成原子操作:
// 释放锁:Lua 脚本保证「校验 requestId + 删除」原子执行 public boolean releaseLock(String lockKey, String requestId) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; // eval 执行脚本,KEYS[1] 是锁键,ARGV[1] 是请求标识 Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return Long.valueOf(1).equals(result); }KEYS[1]传锁键名,ARGV[1]传requestId。脚本先比对值,相等才删,返回 1 表示释放成功,0 表示锁已不属于当前请求。这样即使锁在判断瞬间过期,也不会误删他人锁。这是生产级实现和玩具代码的分水岭。
3.3 续租:看门狗机制与过期时间的博弈
锁的过期时间设短了,业务没跑完锁就没了;设长了,进程崩溃后锁要等很久才释放。源码里引入了续租思路——在业务执行期间,由一个后台任务定期检查锁是否还持有,是则延长过期时间。常见做法是起一个定时线程,每隔expireTime / 3续一次:
// 续租:锁仍属于自己时,重置过期时间 public boolean renewLock(String lockKey, String requestId, int expireTime) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('pexpire', KEYS[1], ARGV[2]) " + "else " + " return 0 " + "end"; Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Arrays.asList(requestId, String.valueOf(expireTime))); return Long.valueOf(1).equals(result); }pexpire以毫秒为单位重置过期时间,同样用 Lua 保证「校验 + 续期」原子。续租间隔不能太密,否则 Redis 压力大;也不能太疏,否则锁先过期了才续,等于没续。一般取过期时间的三分之一。这里有个血泪经验:续租线程必须和业务线程绑定生命周期,业务结束要能停掉续租,否则线程泄漏。
3.4 接入 Spring Boot 与 Sentinel 集群
redis-boot-sentinel-cluster模块演示了怎么把锁接到真实环境。Sentinel 模式下,Jedis 连的不是单节点,而是通过 Sentinel 拿到当前 master 地址。YAML 里通常配spring.redis.sentinel.master和nodes列表。锁的代码不用改,改的是连接工厂——把JedisPool换成JedisSentinelPool,锁逻辑照旧。这一步的意义在于:生产环境 Redis 很少单机,主从切换时锁不能丢,Sentinel 能自动把客户端指向新 master。
注意:主从切换存在极短窗口期,若锁刚写入 master 还没同步到 slave 就发生切换,可能出现两个客户端同时持锁。对一致性要求极高的场景,要了解 RedLock 或改用其他方案,这套源码主要覆盖单实例/Sentinel 场景。
4. 避坑与排查:五个真实翻车现场
锁这东西,写出来能跑不代表能用。下面五条都是高并发下真实会遇到的,按「现象 → 原因 → 解决」记。
4.1 锁没生效,接口照样并发
现象:压测时库存还是超卖,日志显示锁「加成功了」。原因:加锁和业务代码不在同一个锁键上,或者锁的粒度太粗/太细,比如用用户 ID 当锁键却去扣全局库存。解决:锁键必须和要保护的资源一一对应,扣哪个商品的库存就用哪个商品 ID 拼锁键,别用固定字符串。
4.2 业务没跑完锁就过期
现象:偶发两个线程同时进入临界区,间隔刚好等于锁过期时间。原因:expireTime设得比业务最长耗时短,锁提前释放。解决:先估算业务 P99 耗时,过期时间设为它的 2~3 倍,同时开启续租兜底。别拍脑袋写 10 秒。
4.3 释放锁报错,删了别人的锁
现象:日志里出现「释放锁失败」但锁确实被删了。原因:没用 Lua 脚本,先GET再DEL,中间锁过期被他人抢走。解决:统一走 Lua 原子释放,requestId必须每次请求唯一,不能用固定值。
4.4 续租线程把服务拖垮
现象:QPS 上来后 Redis 连接数暴涨,CPU 飙高。原因:每个锁请求都起一个独立续租线程,且间隔太短。解决:用共享调度线程池统一续租,间隔设为过期时间的三分之一,业务结束立即取消任务。
4.5 Sentinel 切换后连不上
现象:主从切换后客户端报连接异常,锁全部失效。原因:YAML 里 Sentinel 节点配错,或没配密码。解决:核对nodes列表和master名称,确认 Sentinel 与 Redis 密码一致,切换后让连接池自动重连,别手动缓存 master 地址。
5. 进阶验证:怎么确认你的锁真的扛得住并发
写完锁,最怕的是「看起来对」。我一般会做两件事验证:一是用多线程模拟并发抢锁,二是用 Redis 客户端观察键的 TTL 变化。
先写一个并发测试,起 50 个线程抢同一把锁,统计成功次数:
// 并发验证:50 个线程抢同一把锁,成功数应恒为 1 ExecutorService pool = Executors.newFixedThreadPool(50); AtomicInteger successCount = new AtomicInteger(0); CountDownLatch latch = new CountDownLatch(50); for (int i = 0; i < 50; i++) { pool.submit(() -> { try { String requestId = UUID.randomUUID().toString(); // 每个线程用独立 requestId 抢锁 if (lock.tryLock("stock:1001", requestId, 30000)) { successCount.incrementAndGet(); Thread.sleep(100); // 模拟业务 lock.releaseLock("stock:1001", requestId); } } catch (Exception e) { e.printStackTrace(); } finally { latch.countDown(); } }); } latch.await(); System.out.println("抢锁成功次数:" + successCount.get());CountDownLatch保证 50 个线程同时起跑,successCount在任意时刻只应为 1。如果大于 1,说明锁没生效。跑完再打开 Redis 可视化客户端(比如 Another Redis Desktop Manager),观察stock:1001这个键:加锁时存在且有 TTL,释放后消失,续租时 TTL 被重置。TTL 一直是 -1 说明过期时间没设上,是死锁前兆。
进阶一点,可以故意在业务里Thread.sleep超过过期时间,看续租有没有兜住;再把续租关掉,看是否出现并发进入。这两种对照实验做完,你对这套锁的边界就有数了。从那以后我每次接分布式锁,都强制先跑一遍并发计数和 TTL 观察,再上压测。希望帮到你。
本文还有配套的精品资源,点击获取