生产级Java Redis分布式锁源码实战:从SETNX到Lua原子释放与续租
简介这份源码解析资源面向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 mvnwWindows 下直接用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是本次请求的唯一标识通常用 UUIDexpireTime是过期毫秒数。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 耗时过期时间设为它的 23 倍同时开启续租兜底。别拍脑袋写 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 观察再上压测。希望帮到你。本文还有配套的精品资源点击获取