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.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
package com.example.lock;
import java.io.Closeable;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 基于Redis的分布式锁实现.
* 支持自旋等待、超时释放、安全解锁.
*/
public class DistributedLock implements Closeable {
private static final Logger log = LoggerFactory.getLogger(DistributedLock.class);
private static final String LOCK_PREFIX = "app-locks:{%s}";
private static final String REQUEST_ID_KEY = "rid-{%s}";
/** 加锁Lua脚本:key不存在时设置hash并设过期时间. */
private static final String LOCK_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. */
private static final String UNLOCK_SCRIPT =
"if redis.call('hget', KEYS[1], KEYS[2]) == ARGV[1] then "
+ "redis.call('del', KEYS[1]); return 1; "
+ "else return 0; end;";
private final StringRedisTemplate redisTemplate;
private final String key;
private final String lockKey;
private final String requestIdKey;
private final String requestId;
private TimeUnit leaseTimeUnit = TimeUnit.MINUTES;
private long leaseTime = 10;
private boolean locked = false;
DistributedLock(StringRedisTemplate redisTemplate, String key, String requestId) {
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);
}
void setLeaseTimeUnit(TimeUnit leaseTimeUnit) {
if (leaseTimeUnit != null) {
this.leaseTimeUnit = leaseTimeUnit;
}
}
void setLeaseTime(long leaseTime) {
if (leaseTime > 0) {
this.leaseTime = leaseTime;
}
}
/**
* 尝试获取锁,支持超时等待.
*
* @param waitTimeUnit 等待时间单位
* @param waitTime 最大等待时间
* @return true-获取成功,false-超时未获取到
*/
public boolean tryLock(TimeUnit waitTimeUnit, long waitTime) {
Boolean result = redisTemplate.execute(connection -> {
long waitMillis = waitTimeUnit.toMillis(waitTime);
long startTime = System.currentTimeMillis();
long deadline = startTime + waitMillis;
// 第一次尝试
if (doLock(connection)) {
return Boolean.TRUE;
}
// 自旋重试直到超时
while (System.currentTimeMillis() < deadline) {
if (doLock(connection)) {
return Boolean.TRUE;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error("获取锁等待时被中断", e);
Thread.currentThread().interrupt();
return Boolean.FALSE;
}
}
return Boolean.FALSE;
});
this.locked = Boolean.TRUE.equals(result);
log.debug("获取锁结果 - {} - {}", key, locked);
return this.locked;
}
/**
* 释放锁.
*
* @return true-释放成功,false-释放失败(非持有者或已过期)
*/
public boolean unlock() {
if (!locked) {
log.info("未锁,无需操作 - {}", key);
return true;
}
log.info("准备释放锁 - {}", key);
Boolean result = redisTemplate.execute(connection -> {
long expireMillis = leaseTimeUnit.toMillis(leaseTime);
Long evalResult = (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);
return evalResult != null && evalResult != 0 ? Boolean.TRUE : Boolean.FALSE;
});
this.locked = !Boolean.TRUE.equals(result);
return Boolean.TRUE.equals(result);
}
@Override
public void close() {
unlock();
}
private boolean doLock(org.springframework.data.redis.connection.RedisConnection connection) {
long expireMillis = leaseTimeUnit.toMillis(leaseTime);
Long evalResult = (Long) connection.eval(
LOCK_SCRIPT.getBytes(),
org.springframework.data.redis.connection.ReturnType.INTEGER,
2,
lockKey.getBytes(),
requestIdKey.getBytes(),
requestId.getBytes(),
String.valueOf(expireMillis).getBytes()
);
return evalResult != null && evalResult != 0;
}
}
DistributedLockProvider.java
package com.example.lock;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 分布式锁工厂.
* 通过注入此类获取锁实例.
*/
public class DistributedLockProvider {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取分布式锁(默认过期10分钟).
*
* @param key 业务锁标识
* @return 锁实例
*/
public DistributedLock getLock(String key) {
return new DistributedLock(redisTemplate, key, getCurrentRequestId());
}
/**
* 获取分布式锁(自定义过期时间).
*
* @param key 业务锁标识
* @param leaseTimeUnit 过期时间单位
* @param leaseTime 过期时间值
* @return 锁实例
*/
public DistributedLock getLock(String key, TimeUnit leaseTimeUnit, long leaseTime) {
DistributedLock lock = new DistributedLock(redisTemplate, key, getCurrentRequestId());
lock.setLeaseTime(leaseTime);
lock.setLeaseTimeUnit(leaseTimeUnit);
return lock;
}
private String getCurrentRequestId() {
// 从链路追踪上下文获取requestId,保证同一请求内的锁标识一致
// 实际项目中可替换为 MDC.get("requestId") 或自定义实现
return java.util.UUID.randomUUID().toString().replace("-", "");
}
}
LockConfig.java
package com.example.lock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 分布式锁自动配置类.
*/
@Configuration
public class LockConfig {
@Bean
public DistributedLockProvider distributedLockProvider() {
return new DistributedLockProvider();
}
}
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: localhost
port: 6379
password: your_password
database: 0
使用示例1:try-with-resources 自动释放(最佳实践)
@Service
public class OrderService {
@Resource
private DistributedLockProvider distributedLockProvider;
public void processOrder(String orderId) {
String lockKey = "order_process_" + orderId;
// 锁过期5分钟,等待获取锁最多3分钟
try (DistributedLock lock = distributedLockProvider.getLock(
lockKey, TimeUnit.MINUTES, 5)) {
if (lock.tryLock(TimeUnit.MINUTES, 3)) {
// 获取锁成功,安全执行业务逻辑
doProcessOrder(orderId);
} else {
// 超时未获取到锁
throw new RuntimeException("订单正在处理中,请勿重复操作");
}
}
// try 块结束自动调用 close() -> unlock()
}
}
使用示例2:非阻塞立即返回
public void submitTask(String taskId) {
String lockKey = "task_submit_" + taskId;
try (DistributedLock lock = distributedLockProvider.getLock(lockKey)) {
// 等待时间为0,立即返回结果
if (lock.tryLock(TimeUnit.MILLISECONDS, 0)) {
executeTask(taskId);
} else {
throw new RuntimeException("任务已在执行中");
}
}
}
使用示例3:事务提交后释放锁
@Transactional
public void updateStock(Integer itemId) {
String key = "stock_update_" + itemId;
DistributedLock lock = distributedLockProvider.getLock(key, TimeUnit.MINUTES, 3);
if (!lock.tryLock(TimeUnit.MINUTES, 1)) {
throw new RuntimeException("库存操作冲突");
}
// 在事务提交后再释放锁,保证其他线程读到的是已提交数据
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
lock.unlock();
}
});
// 执行库存扣减
doUpdateStock(itemId);
}
使用示例4:MQ 消费防重复处理
@Component
public class OrderMqConsumer {
@Resource
private DistributedLockProvider distributedLockProvider;
public void onMessage(Map<String, Object> message) {
String orderId = (String) message.get("orderId");
String lockKey = "mq_consume_order_" + orderId;
// 锁10分钟,等待8分钟
try (DistributedLock lock = 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 | 轻量无侵入 |
更多推荐
所有评论(0)