news 2026/7/24 2:00:06

Spring Boot Starter实现基于 Redis + Lua 的分布式锁封装与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot Starter实现基于 Redis + Lua 的分布式锁封装与实践

Spring Boot Starter实现基于 Redis + Lua 的分布式锁封装与实践


一、涉及的技术知识点

1.1 Redis 分布式锁核心原理

知识点说明
Redis 单线程模型保证命令按序执行,天然支持原子操作
SETNX(SET if Not eXists)只有 key 不存在时才设置成功,是分布式锁的基础
PEXPIRE设置 key 毫秒级过期时间,防止死锁
HSET/HGET用 Hash 结构存储锁持有者标识(requestId),支持安全释放
Lua 脚本多个 Redis 命令在服务端原子执行,保证加锁/解锁操作的原子性
锁超时自动释放持有者崩溃时锁不会永久占用
可重入防护通过 requestId 标识持有者,只有持有者能释放锁

1.2 Spring Boot 自动配置机制

知识点说明
spring.factoriesSpring Boot SPI 加载机制,声明自动配置类
@Configuration+@Bean声明式注册 Spring Bean
@Autowired/@Resource自动注入StringRedisTemplate
条件装配可配合@ConditionalOnClass实现按需加载

1.3 Java 语言特性

知识点说明
Closeable接口实现自动资源管理,支持 try-with-resources
Lambda 表达式RedisCallback用 Lambda 简化回调写法
自旋等待 + 超时退出tryLock内循环重试直到超时
包级访问控制构造器和 setter 使用包级权限,只允许通过 Provider 创建

1.4 设计模式

模式应用
工厂模式DistributedLockProvider作为工厂创建DistributedLock实例
模板方法StringRedisTemplate.execute(RedisCallback)统一连接管理
策略模式不同TimeUnit+leaseTime组合适应不同场景

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、封装 Spring Boot Starter JAR 包的流程

2.1 项目结构

xxx-xxx-lib-lock/ ├── pom.xml └── src/main/ ├── java/xx/xx/lib/lock/ │ ├── LockConfig.java // 自动配置类 │ ├── DistributedLockProvider.java // 锁工厂(对外暴露) │ └── DistributedLock.java // 锁实现(核心逻辑) └── resources/META-INF/ └── spring.factories // 自动配置声明

2.2 封装步骤

  1. 定义核心接口/类— 确定对外暴露的 API(Provider + Lock)
  2. 实现核心逻辑— Redis Lua 脚本加锁解锁
  3. 编写自动配置@Configuration类注册 Bean
  4. 声明 spring.factories— 让 Spring Boot 自动发现配置类
  5. Maven 打包发布mvn deploy到私有仓库

2.3 引入方使用流程

  1. pom.xml添加依赖
  2. 确保项目已配置spring.redisStringRedisTemplate可用)
  3. 注入DistributedLockProvider直接使用
  4. 无需任何额外配置或注解开启

三、通用示例代码

3.1 Starter 封装侧

pom.xml
<project><groupId>com.example</groupId><artifactId>example-distributed-lock-starter</artifactId><version>1.0.0</version><packaging>jar</packaging><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><scope>provided</scope></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><scope>provided</scope></dependency></dependencies></project>
DistributedLock.java
packagecom.example.lock;importjava.io.Closeable;importjava.util.concurrent.TimeUnit;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.data.redis.core.StringRedisTemplate;/** * 基于Redis的分布式锁实现. * 支持自旋等待、超时释放、安全解锁. */publicclassDistributedLockimplementsCloseable{privatestaticfinalLoggerlog=LoggerFactory.getLogger(DistributedLock.class);privatestaticfinalStringLOCK_PREFIX="app-locks:{%s}";privatestaticfinalStringREQUEST_ID_KEY="rid-{%s}";/** 加锁Lua脚本:key不存在时设置hash并设过期时间. */privatestaticfinalStringLOCK_SCRIPT="if (redis.call('exists', KEYS[1]) == 0) then "+"redis.call('hset', KEYS[1], KEYS[2], ARGV[1]); "+"redis.call('pexpire', KEYS[1], ARGV[2]); "+"return 1; "+"else return 0; end";/** 解锁Lua脚本:校验requestId匹配后删除key. */privatestaticfinalStringUNLOCK_SCRIPT="if redis.call('hget', KEYS[1], KEYS[2]) == ARGV[1] then "+"redis.call('del', KEYS[1]); return 1; "+"else return 0; end;";privatefinalStringRedisTemplateredisTemplate;privatefinalStringkey;privatefinalStringlockKey;privatefinalStringrequestIdKey;privatefinalStringrequestId;privateTimeUnitleaseTimeUnit=TimeUnit.MINUTES;privatelongleaseTime=10;privatebooleanlocked=false;DistributedLock(StringRedisTemplateredisTemplate,Stringkey,StringrequestId){this.redisTemplate=redisTemplate;this.key=key;this.requestId=(requestId==null||requestId.isEmpty())?java.util.UUID.randomUUID().toString().replace("-",""):requestId;this.lockKey=String.format(LOCK_PREFIX,key);this.requestIdKey=String.format(REQUEST_ID_KEY,key);}voidsetLeaseTimeUnit(TimeUnitleaseTimeUnit){if(leaseTimeUnit!=null){this.leaseTimeUnit=leaseTimeUnit;}}voidsetLeaseTime(longleaseTime){if(leaseTime>0){this.leaseTime=leaseTime;}}/** * 尝试获取锁,支持超时等待. * * @param waitTimeUnit 等待时间单位 * @param waitTime 最大等待时间 * @return true-获取成功,false-超时未获取到 */publicbooleantryLock(TimeUnitwaitTimeUnit,longwaitTime){Booleanresult=redisTemplate.execute(connection->{longwaitMillis=waitTimeUnit.toMillis(waitTime);longstartTime=System.currentTimeMillis();longdeadline=startTime+waitMillis;// 第一次尝试if(doLock(connection)){returnBoolean.TRUE;}// 自旋重试直到超时while(System.currentTimeMillis()<deadline){if(doLock(connection)){returnBoolean.TRUE;}try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){log.error("获取锁等待时被中断",e);Thread.currentThread().interrupt();returnBoolean.FALSE;}}returnBoolean.FALSE;});this.locked=Boolean.TRUE.equals(result);log.debug("获取锁结果 - {} - {}",key,locked);returnthis.locked;}/** * 释放锁. * * @return true-释放成功,false-释放失败(非持有者或已过期) */publicbooleanunlock(){if(!locked){log.info("未锁,无需操作 - {}",key);returntrue;}log.info("准备释放锁 - {}",key);Booleanresult=redisTemplate.execute(connection->{longexpireMillis=leaseTimeUnit.toMillis(leaseTime);LongevalResult=(Long)connection.eval(UNLOCK_SCRIPT.getBytes(),org.springframework.data.redis.connection.ReturnType.INTEGER,2,lockKey.getBytes(),requestIdKey.getBytes(),requestId.getBytes());log.info("释放锁数量 - {} - {} - {}",requestId,key,evalResult);returnevalResult!=null&&evalResult!=0?Boolean.TRUE:Boolean.FALSE;});this.locked=!Boolean.TRUE.equals(result);returnBoolean.TRUE.equals(result);}@Overridepublicvoidclose(){unlock();}privatebooleandoLock(org.springframework.data.redis.connection.RedisConnectionconnection){longexpireMillis=leaseTimeUnit.toMillis(leaseTime);LongevalResult=(Long)connection.eval(LOCK_SCRIPT.getBytes(),org.springframework.data.redis.connection.ReturnType.INTEGER,2,lockKey.getBytes(),requestIdKey.getBytes(),requestId.getBytes(),String.valueOf(expireMillis).getBytes());returnevalResult!=null&&evalResult!=0;}}
DistributedLockProvider.java
packagecom.example.lock;importjava.util.concurrent.TimeUnit;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;/** * 分布式锁工厂. * 通过注入此类获取锁实例. */publicclassDistributedLockProvider{@AutowiredprivateStringRedisTemplateredisTemplate;/** * 获取分布式锁(默认过期10分钟). * * @param key 业务锁标识 * @return 锁实例 */publicDistributedLockgetLock(Stringkey){returnnewDistributedLock(redisTemplate,key,getCurrentRequestId());}/** * 获取分布式锁(自定义过期时间). * * @param key 业务锁标识 * @param leaseTimeUnit 过期时间单位 * @param leaseTime 过期时间值 * @return 锁实例 */publicDistributedLockgetLock(Stringkey,TimeUnitleaseTimeUnit,longleaseTime){DistributedLocklock=newDistributedLock(redisTemplate,key,getCurrentRequestId());lock.setLeaseTime(leaseTime);lock.setLeaseTimeUnit(leaseTimeUnit);returnlock;}privateStringgetCurrentRequestId(){// 从链路追踪上下文获取requestId,保证同一请求内的锁标识一致// 实际项目中可替换为 MDC.get("requestId") 或自定义实现returnjava.util.UUID.randomUUID().toString().replace("-","");}}
LockConfig.java
packagecom.example.lock;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/** * 分布式锁自动配置类. */@ConfigurationpublicclassLockConfig{@BeanpublicDistributedLockProviderdistributedLockProvider(){returnnewDistributedLockProvider();}}
META-INF/spring.factories
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.lock.LockConfig

3.2 引入方使用侧

pom.xml 添加依赖
<dependency><groupId>com.example</groupId><artifactId>example-distributed-lock-starter</artifactId><version>1.0.0</version></dependency>
application.yml(确保 Redis 已配置)
spring:redis:host:localhostport:6379password:your_passworddatabase:0
使用示例1:try-with-resources 自动释放(最佳实践)
@ServicepublicclassOrderService{@ResourceprivateDistributedLockProviderdistributedLockProvider;publicvoidprocessOrder(StringorderId){StringlockKey="order_process_"+orderId;// 锁过期5分钟,等待获取锁最多3分钟try(DistributedLocklock=distributedLockProvider.getLock(lockKey,TimeUnit.MINUTES,5)){if(lock.tryLock(TimeUnit.MINUTES,3)){// 获取锁成功,安全执行业务逻辑doProcessOrder(orderId);}else{// 超时未获取到锁thrownewRuntimeException("订单正在处理中,请勿重复操作");}}// try 块结束自动调用 close() -> unlock()}}
使用示例2:非阻塞立即返回
publicvoidsubmitTask(StringtaskId){StringlockKey="task_submit_"+taskId;try(DistributedLocklock=distributedLockProvider.getLock(lockKey)){// 等待时间为0,立即返回结果if(lock.tryLock(TimeUnit.MILLISECONDS,0)){executeTask(taskId);}else{thrownewRuntimeException("任务已在执行中");}}}
使用示例3:事务提交后释放锁
@TransactionalpublicvoidupdateStock(IntegeritemId){Stringkey="stock_update_"+itemId;DistributedLocklock=distributedLockProvider.getLock(key,TimeUnit.MINUTES,3);if(!lock.tryLock(TimeUnit.MINUTES,1)){thrownewRuntimeException("库存操作冲突");}// 在事务提交后再释放锁,保证其他线程读到的是已提交数据TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronization(){@OverridepublicvoidafterCommit(){lock.unlock();}});// 执行库存扣减doUpdateStock(itemId);}
使用示例4:MQ 消费防重复处理
@ComponentpublicclassOrderMqConsumer{@ResourceprivateDistributedLockProviderdistributedLockProvider;publicvoidonMessage(Map<String,Object>message){StringorderId=(String)message.get("orderId");StringlockKey="mq_consume_order_"+orderId;// 锁10分钟,等待8分钟try(DistributedLocklock=distributedLockProvider.getLock(lockKey,TimeUnit.MINUTES,10)){if(lock.tryLock(TimeUnit.MINUTES,8)){processMessage(message);}else{log.warn("消息正在被其他消费者处理: {}",orderId);}}}}

四、设计要点总结

要点实现方式收益
零配置使用spring.factories 自动装配引入依赖即可用,无需@Enable注解
防死锁锁自带过期时间进程崩溃不会永久阻塞
防误释放requestId 校验A 加的锁不会被 B 释放
原子操作Lua 脚本避免 check-then-act 竞态条件
资源安全Closeable + try-with-resources确保锁一定被释放
灵活度工厂模式 + 可配参数不同场景可设不同超时
可观测性关键操作打日志方便排查锁竞争问题
最小依赖仅依赖 spring-data-redis + slf4j轻量无侵入
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/24 1:58:36

高校AI教改:智能体与AIGC实验平台选择指南

近年来&#xff0c;人工智能技术飞速发展&#xff0c;深刻影响着各行各业&#xff0c;教育领域亦不例外。教育部《“人工智能教育”行动计划》的深入推进&#xff0c;使得AI教学改革成为高校教育创新的重要方向。在这一背景下&#xff0c;如何为AI教改课题选择一个能够高效支撑…

作者头像 李华
网站建设 2026/7/24 1:58:16

UnityWebRequest安全连接异常解决方案:从ATS策略到HTTPS配置

1. 项目概述&#xff1a;当UnityWebRequest遇上“不安全连接”在Unity开发中&#xff0c;尤其是涉及到网络通信的移动端、PC端应用或需要与后端API交互的项目里&#xff0c;UnityWebRequest是我们最常打交道的类之一。它封装了HTTP请求的发送与接收&#xff0c;比古老的WWW类更…

作者头像 李华
网站建设 2026/7/24 1:57:01

NLP文本清洗实战:从编码规范到质量监控

1. 文本处理的核心挑战与价值脏数据就像厨房里没洗的蔬菜——表面沾满泥土但内在价值巨大。在自然语言处理领域&#xff0c;我们每天面对的真实文本数据中&#xff0c;约78%存在各种形式的"脏污"&#xff1a;从简单的空格错位到复杂的编码混乱&#xff0c;从无意义的…

作者头像 李华
网站建设 2026/7/24 1:51:05

速卖通图片批量翻译的Python实现方案

一、问题引入许多速卖通卖家在上架商品时&#xff0c;都面临一个共同的难题&#xff1a;如何高效地将商品图中的文字翻译成目标市场语言。传统做法是手动修图或用Photoshop逐一替换文字&#xff0c;一个熟练的美工每天最多处理30-50张图片&#xff0c;遇到需要同时上架200款新品…

作者头像 李华