当前位置: 首页 > news >正文

Java 高并发场景下 Redis 分布式锁(UUID+Lua)最佳实践

一、核心原理:Redis 分布式锁的设计基石

1.1 分布式锁的核心要求

一款可靠的分布式锁需满足以下 4 点核心要求,否则易引发死锁、锁误删、数据不一致等问题:

  • 互斥性:同一时间只有一个线程能持有锁,杜绝并发竞争;

  • 安全性:仅持有锁的线程能释放锁,防止误删其他线程的锁;

  • 防死锁:锁需设置过期时间,避免线程持有锁后宕机导致锁永久占用;

  • 高可用:加锁、解锁操作高效,适配高并发场景,不成为性能瓶颈。

1.2 UUID+Lua 方案的核心逻辑

本方案通过“Redis 原子加锁 + UUID 唯一标识 + Lua 原子解锁”三者结合,满足上述要求:

  1. UUID 唯一标识:作为锁的 Value 值,绑定加锁线程,确保“锁归属唯一”。UUID 全局唯一,可避免分布式场景下(多服务、多线程)锁归属误判,替代线程 ID(进程内唯一,跨进程易重复);

  2. Redis 原子加锁:使用setIfAbsent(SETNX)操作,原子性完成“锁不存在则设置 + 过期时间”,避免加锁与设置过期时间分离导致的死锁;

  3. Lua 原子解锁:通过 Lua 脚本原子执行“判断锁归属 + 删除锁”,避免“判断”与“删除”两步操作分离导致的锁误删。

二、完整实现:通用 Redis 分布式锁工具类

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * 通用 Redis 分布式等待锁工具类(UUID+Lua 方案) * 核心能力:超时等待加锁、原子解锁,适配高并发场景 */ @Component public class GenericRedisWaitLock { private final StringRedisTemplate stringRedisTemplate; // Lua 原子解锁脚本:验证锁归属(UUID)后删除,避免误删 private static final String UNLOCK_LUA_SCRIPT = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """; // 重试间隔(50ms):平衡重试效率与 CPU 占用 private static final long DEFAULT_RETRY_INTERVAL = 50; // 构造器注入 Redis 模板(Spring 自动装配) public GenericRedisWaitLock(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 尝试获取分布式锁(支持自定义等待时间单位,过期时间固定为秒) * @param lockKey 锁键(如:lock:device:1001) * @param waitTime 最大等待时间(超时后放弃) * @param waitTimeUnit 等待时间单位(秒/毫秒等) * @param expireTimeSec 锁过期时间(秒,必须>0,防止死锁) * @return 锁标识(UUID):成功返回标识,失败返回 null */ public String tryLock(String lockKey, long waitTime, TimeUnit waitTimeUnit, long expireTimeSec) { // 入参校验:避免非法参数导致异常 Assert.hasText(lockKey, "lockKey 不能为空"); Assert.isTrue(waitTime >= 0, "waitTime 不能为负数"); Assert.isTrue(expireTimeSec > 0, "expireTimeSec 必须大于 0"); Assert.notNull(waitTimeUnit, "waitTimeUnit 不能为空"); // 生成 UUID 作为锁标识,绑定当前线程 String lockValue = UUID.randomUUID().toString(); // 转换等待时间为毫秒,计算截止时间 long waitTimeMs = waitTimeUnit.toMillis(waitTime); long deadline = System.currentTimeMillis() + waitTimeMs; // 循环尝试加锁,直到超时 while (System.currentTimeMillis() < deadline) { // 原子加锁:不存在则设置值 + 过期时间 Boolean lockSuccess = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, expireTimeSec, TimeUnit.SECONDS); // 加锁成功,返回 UUID 标识(解锁时需传入) if (Boolean.TRUE.equals(lockSuccess)) { return lockValue; } // 加锁失败,短暂休眠后重试(避免 CPU 空转) try { Thread.sleep(DEFAULT_RETRY_INTERVAL); } catch (InterruptedException e) { // 捕获中断异常,恢复线程状态并退出 Thread.currentThread().interrupt(); return null; } } // 等待超时,返回 null 表示加锁失败 return null; } /** * 重载方法:等待时间与过期时间均为秒(极简调用) * @param lockKey 锁键 * @param waitTimeSec 最大等待时间(秒) * @param expireTimeSec 锁过期时间(秒) * @return 锁标识(UUID)/null */ public String tryLock(String lockKey, long waitTimeSec, long expireTimeSec) { return tryLock(lockKey, waitTimeSec, TimeUnit.SECONDS, expireTimeSec); } /** * 原子释放分布式锁 * @param lockKey 锁键(与加锁时一致) * @param lockValue 锁标识(加锁时返回的 UUID) * @return true=解锁成功;false=锁不存在/非当前线程持有 */ public boolean unlock(String lockKey, String lockValue) { // 空值快速失败:避免空指针与无效解锁 if (lockKey == null || lockValue == null) { return false; } // 初始化 Lua 脚本 DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>(); unlockScript.setScriptText(UNLOCK_LUA_SCRIPT); unlockScript.setResultType(Long.class); // 执行 Lua 脚本:原子判断并删除锁 Long executeResult = stringRedisTemplate.execute( unlockScript, Collections.singletonList(lockKey), // KEYS[1] = 锁键 lockValue // ARGV[1] = UUID 标识 ); // 结果为 1 表示解锁成功,0 表示锁归属不匹配/已过期 return executeResult != null && executeResult == 1; } }

三、实战使用:高并发场景示例

以“设备序号递增”和“FDT 巡检记录创建”两个典型高并发场景为例,演示工具类的使用方式,重点体现“加锁-执行业务-解锁”的完整流程。

3.1 场景一:设备序号递增(避免重复/跳号)

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @Service public class DeviceSeqService { private final GenericRedisWaitLock redisWaitLock; private final StringRedisTemplate stringRedisTemplate; // 设备序号缓存键 private static final String DEVICE_SEQ_KEY = "device:seq:current"; // 设备序号锁键 private static final String DEVICE_SEQ_LOCK_KEY = "lock:device:seq"; // 加锁配置:最多等 3 秒,锁过期 10 秒 private static final long WAIT_TIME_SEC = 3; private static final long EXPIRE_TIME_SEC = 10; public DeviceSeqService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) { this.redisWaitLock = redisWaitLock; this.stringRedisTemplate = stringRedisTemplate; } // 生成下一个设备序号(高并发安全) public Long generateNextSeq() { String lockValue = null; try { // 1. 获取分布式锁 lockValue = redisWaitLock.tryLock(DEVICE_SEQ_LOCK_KEY, WAIT_TIME_SEC, EXPIRE_TIME_SEC); if (lockValue == null) { throw new RuntimeException("获取锁超时,序号生成失败"); } // 2. 一查二判三更新(临界区业务) String currentSeqStr = stringRedisTemplate.opsForValue().get(DEVICE_SEQ_KEY); Long currentSeq = currentSeqStr == null ? 0 : Long.parseLong(currentSeqStr); if (currentSeq < 0) { throw new RuntimeException("设备序号异常"); } Long newSeq = currentSeq + 1; stringRedisTemplate.opsForValue().set(DEVICE_SEQ_KEY, newSeq.toString()); return newSeq; } finally { // 3. 最终释放锁(必须在 finally 中,确保锁释放) if (lockValue != null) { redisWaitLock.unlock(DEVICE_SEQ_LOCK_KEY, lockValue); } } } }

3.2 场景二:FDT 巡检记录创建(避免重复创建)

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @Service public class FdtInspectService { private final GenericRedisWaitLock redisWaitLock; private final StringRedisTemplate stringRedisTemplate; // 巡检任务缓存键前缀(设备 ID 为后缀) private static final String INSPECT_TASK_PREFIX = "fdt:inspect:task:"; // 巡检锁键前缀(设备 ID 为后缀,细粒度锁) private static final String INSPECT_LOCK_PREFIX = "lock:fdt:inspect:"; // 加锁配置 private static final long WAIT_TIME_SEC = 3; private static final long EXPIRE_TIME_SEC = 10; public FdtInspectService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) { this.redisWaitLock = redisWaitLock; this.stringRedisTemplate = stringRedisTemplate; } // 创建设备巡检记录(高并发安全) public boolean createInspectTask(String deviceId) { String lockKey = INSPECT_LOCK_PREFIX + deviceId; String taskKey = INSPECT_TASK_PREFIX + deviceId; String lockValue = null; try { // 1. 获取设备维度的细粒度锁(不影响其他设备并发) lockValue = redisWaitLock.tryLock(lockKey, WAIT_TIME_SEC, EXPIRE_TIME_SEC); if (lockValue == null) { return false; // 加锁失败,说明已有采集器处理该设备 } // 2. 一查二判三更新(临界区业务) String taskStatus = stringRedisTemplate.opsForValue().get(taskKey); if ("RUNNING".equals(taskStatus)) { return false; // 已存在巡检任务,不重复创建 } // 创建设备任务(设置为运行中状态,有效期 30 分钟) stringRedisTemplate.opsForValue().set(taskKey, "RUNNING", 30, java.util.concurrent.TimeUnit.MINUTES); // 模拟巡检记录入库逻辑 saveInspectTaskToDb(deviceId); return true; } finally { // 3. 释放锁 if (lockValue != null) { redisWaitLock.unlock(lockKey, lockValue); } } } // 模拟巡检记录入库 private void saveInspectTaskToDb(String deviceId) { // 实际业务中替换为数据库写入逻辑 } }

四、关键注意事项与最佳实践

4.1 锁的粒度设计

优先使用细粒度锁(如场景二中按设备 ID 加锁),而非全局锁。细粒度锁可减少锁竞争,提升系统并发能力;全局锁会导致所有线程排队,成为性能瓶颈。

4.2 过期时间设置

锁过期时间(expireTimeSec)需满足:过期时间 > 业务最大耗时,建议设置为业务最大耗时的 1.5-2 倍。例如业务最多执行 5 秒,过期时间可设为 10 秒,避免锁提前过期导致并发问题。

4.3 解锁操作规范

  • 解锁必须传入加锁时返回的 UUID 标识,否则 Lua 脚本会拒绝解锁,防止误删;

  • 解锁操作必须放在finally块中,确保无论业务执行成功还是异常,锁都能最终释放;

  • 解锁失败仅需日志记录,无需抛异常(锁已自动过期,不影响数据一致性)。

4.4 加锁失败处理

加锁失败(返回 null)时,可根据业务场景选择处理方式:

  • 抛异常:适用于必须获取锁才能执行的业务(如序号生成);

  • 返回失败:适用于非核心业务(如巡检记录创建);

  • 有限重试:通过循环重试获取锁(需控制重试次数,避免无限循环)。

4.5 避免常见坑

  • 不直接使用delete解锁:直接删除锁会导致误删其他线程的锁,必须用 Lua 脚本原子解锁;

  • 不忽略中断异常:捕获InterruptedException后需恢复线程中断状态,避免线程状态错乱;

  • 不使用线程 ID 作为锁标识:线程 ID 跨进程易重复,无法满足分布式场景需求。

五、总结

Redis 分布式锁(UUID+Lua)方案,通过 UUID 保证锁归属唯一,通过 Lua 脚本保证解锁原子性,结合 Redis 高性能原子操作,可完美解决分布式高并发场景下的数据一致性问题。本文提供的工具类轻量化、无业务耦合,可直接复用;实战示例覆盖了常见高并发场景,给出了细粒度锁、过期时间配置等最佳实践。

在实际开发中,需根据业务特点调整加锁等待时间、过期时间,合理设计锁粒度,避开常见坑点,才能充分发挥该方案的优势,构建高可用、高并发的分布式系统。

http://www.jsqmd.com/news/659446/

相关文章:

  • 超级千问语音设计世界优化升级:使用Nginx反向代理提升访问安全
  • NoSQL之Redis配置与优化
  • 最新的Claude-opus-4-7在科研场景到底有多强...
  • Qwen3.5-9B零基础部署:5分钟本地跑通,笔记本也能玩转原生多模态
  • MGeo模型效果展示:支持‘北京市海淀区五道口地铁站A口’等交通节点地址解析
  • 某宝登录密码加密逆向实战——从password2到st码的完整流程解析
  • 2026螺栓厂家推荐排行榜产能与专利双优企业领跑(全国调研) - 爱采购寻源宝典
  • 从宏到constexpr:Visual Studio代码分析规则C26432的实战解读
  • SITS2026未公开技术纪要:为什么92%的AI编程工具在遗留系统中失效?3个架构适配公式+2个轻量改造模板
  • AI 答疑助手优化实践:从 RAG 到 LightRAG 的全链路升级
  • 一个插件,国内直接用Claude Opus 4.7
  • 重生之从0开始学习c++之模板初级
  • 2026玻璃钢地埋式管道厂家推荐 河北博翔产能领先+专利护航+服务全面 - 爱采购寻源宝典
  • Stable Diffusion Anything V5保姆级教学:快速搭建AI绘画平台
  • 当Copilot写出恶意反序列化代码时——智能代码生成安全风险评估的“黄金45分钟”响应协议(含SAST+DAST+LLM-Sandbox三重验证机制)
  • Golang go mod vendor怎么用_Golang vendor教程【必备】
  • 不用人类训练?这款开源大模型已开启自我进化
  • 全栈开发vs垂直领域:2026收益对比
  • 2026风道加热器厂家推荐排行榜江苏中仁以产能、专利、服务三维度领跑全国 - 爱采购寻源宝典
  • 别再手动拖模型了!Babylon.js Scene Loader 动态注册与按需加载实战(附NPM最佳配置)
  • 2026排水沟厂家推荐排行榜河北欧意科技领衔,产能与专利双优认证 - 爱采购寻源宝典
  • Phi-4-mini-reasoning百度SEO适配:技术博文如何用其生成高质量内容
  • AISQL生成不是噱头,是生产力革命:37个真实生产环境SQL生成失败案例全复盘
  • 双膜储气柜:专业生产与品质保障
  • Pixel Couplet Gen惊艳案例:海外华人社区用Pixel Couplet Gen传播春节文化
  • 苏州大学联合百度提出Flux Attention
  • 2026 三相油浸式变压器 厂家推荐 权威榜单(产能/专利/质量三维度对比) - 爱采购寻源宝典
  • Qwen3-Reranker应用案例:AI编程助手中的代码片段语义重排序实践
  • Jmeter 的使用
  • 2026电加热器厂家推荐排行榜从产能规模到专利技术权威解析 - 爱采购寻源宝典