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.factories | Spring 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 封装步骤
- 定义核心接口/类— 确定对外暴露的 API(Provider + Lock)
- 实现核心逻辑— Redis Lua 脚本加锁解锁
- 编写自动配置—
@Configuration类注册 Bean - 声明 spring.factories— 让 Spring Boot 自动发现配置类
- Maven 打包发布—
mvn deploy到私有仓库
2.3 引入方使用流程
pom.xml添加依赖- 确保项目已配置
spring.redis(StringRedisTemplate可用) - 注入
DistributedLockProvider直接使用 - 无需任何额外配置或注解开启
三、通用示例代码
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 | 轻量无侵入 |