easy-lock 使用文档
easy-lock 提供注解驱动 + 编程式两套 API,底层支持 Redis 单 key / Redis 多 key / JDBC 单 key 三种后端,内置重试装饰器 + SpEL key 生成 + 间隔单位转换 + 自动续期能力。核心接口
Lock抽象 acquire / release / refresh 三操作,后端可替换。
1. 引入依赖
<!-- 核心依赖(必需) -->
<dependency>
<groupId>io.matrix</groupId>
<artifactId>easy-lock</artifactId>
<version>${revision}</version>
</dependency>
<!-- Redis 后端(optional scope,需要 RedisTemplate) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JDBC 后端(optional scope,需要 JdbcTemplate) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
三种后端均为 optional scope,编译时不强依赖,运行时按需引入。引入哪个取决于你的 Spring Boot auto-configuration 有没有对应的 ConnectionFactory。
2. 选择后端
| 后端 | 适合场景 | 前置要求 |
|---|---|---|
| Redis(推荐) | 分布式高并发、毫秒级锁、自动过期 | spring-boot-starter-data-redis + Redis 实例 |
| JDBC | 无 Redis、对锁性能要求低 | spring-boot-starter-jdbc + 锁表 DDL |
| 不指定(AUTO) | 让框架自动扫描容器中可用的后端 | 至少引入一种后端 |
3. 配置
3.1 Redis 后端(推荐)
# application.yml
easy:
lock:
# type=redis 表示使用 Redis 后端;AUTO(默认)按容器自动选择
# LockType.REDIS / LockType.JDBC / LockType.AUTO
default-type: redis
# Redis 连接(标准 Spring Boot 配置)
spring:
redis:
host: localhost
port: 6379
3.2 JDBC 后端
easy:
lock:
default-type: jdbc
# JDBC 数据源(标准 Spring Boot 配置)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: xxx
锁表 DDL(每个 storeId 对应一张锁表,按需创建):
CREATE TABLE lock_tbl (
lock_key VARCHAR(255) PRIMARY KEY,
token VARCHAR(255) NOT NULL,
expireAt DATETIME NOT NULL
) ENGINE=InnoDB;
3.3 后端选择优先级
| LockType | 行为 |
|---|---|
AUTO(默认) | Spring 容器里只要有 StringRedisTemplate 就选 Redis;否则有 JdbcTemplate 就选 JDBC;都没有报错 |
REDIS | 强制 Redis,无 StringRedisTemplate 则 Bean 创建失败 |
JDBC | 强制 JDBC,无 JdbcTemplate 则 Bean 创建失败 |
4. 注解驱动(最常用)
@Service
public class OrderService {
// 最简单:方法结束自动释放,无重试
@Locked(expression = "#order.id")
public void create(Order order) {
// ...
}
// 带重试 + 条件不满足不获取
@Locked(
expression = "#order.id",
storeId = "order-lock",
expiration = 60000L, // 60 秒过期
timeout = 30000L, // 总超时 30 秒
retry = 200L, // 每 200ms 重试一次
type = LockType.AUTO
)
public void pay(Order order) {
// 长时间操作...
}
// 手动释放(业务控制释放时机)
@Locked(expression = "#order.id", manuallyReleased = true)
public String startProcess(Order order) {
// 返回 token 给调用方,由调用方决定何时 release
return "token-for-later-release";
}
}
@Locked 参数说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
expression | "" | SpEL 表达式 → List<String>(锁 key 列表);空字符串取默认 key |
storeId | "lock" | 存储维度:Redis 用作 storeId:key 前缀;JDBC 用作表名 |
expiration | 60000L | 锁过期毫秒 |
timeout | 30000L | 重试总超时毫秒(Spring Retry TimeoutRetryPolicy) |
retry | 100L | 重试间隔毫秒 |
refresh | 10000L | 刷新间隔毫秒(框架不内置 watchdog,refresh 需业务显式调) |
prefix | "" | key 前缀(追加到 expression 结果之前) |
type | LockType.AUTO | 后端选择(AUTO/REDIS/JDBC) |
manuallyReleased | false | false=方法结束自动释放;true=业务控制释放 |
5. 编程式 API
5.1 Redis 单 key
@Configuration
public class LockConfig {
@Bean
public Lock redisLock(StringRedisTemplate redis) {
return new SimpleRedisLock(
() -> UUID.randomUUID().toString(), // token 生成策略
redis
);
}
}
@Service
public class OrderService {
@Autowired private Lock redisLock;
public void create(Order order) {
String token = redisLock.acquire(
List.of(order.getId()), // keys(SimpleRedisLock 必须单 key)
"order-lock", // storeId → Redis 前缀 "order-lock:"
60000 // expiration
);
if (token != null) {
try {
// 业务逻辑
} finally {
redisLock.release(List.of(order.getId()), "order-lock", token);
}
} else {
throw new LockNotAvailableException("订单锁获取失败");
}
}
}
5.2 Redis 多 key
@Bean
public MultiRedisLock multiLock(StringRedisTemplate redis) {
return new MultiRedisLock(redis); // 或 (redis, () -> "fixed-token")
}
// 使用:大括号 hash tag 保证 Redis Cluster 下多 key 安全
MultiRedisLock lock = ...;
String token = lock.acquire(
Arrays.asList("account-A", "account-B"),
"account", // Redis key 格式 {account}.account-A / {account}.account-B
30000
);
注意:
SimpleRedisLock是单 key,调用acquire(List.of("k1", "k2"), ...)会抛IllegalArgumentException。多 key 必须用MultiRedisLock。
5.3 JDBC 单 key
@Bean
public SimpleJdbcLock jdbcLock(JdbcTemplate jdbc) {
return new SimpleJdbcLock(
() -> UUID.randomUUID().toString(), // token
new SimpleJdbcLockSingleKeyService(jdbc)
);
}
5.4 手动管理 RetryableLock 装饰器
// 不通过 @Locked 注解,手动构造带重试的 Lock
RetryTemplate rt = RetryTemplate.builder()
.maxAttempts(5)
.fixedBackoff(200)
.retryOn(LockNotAvailableException.class)
.build();
Lock retryable = new RetryableLock(redisLock, rt);
String token = retryable.acquire(List.of("order-1"), "order-lock", 60000);
6. 配置项详解
easy-lock 没有独立的 easy.lock.* ConfigurationProperties,配置通过 @Locked 注解参数和 Spring Boot 自动装配驱动。
| 配置维度 | 方式 | 说明 |
|---|---|---|
| 后端选择 | @Locked(type=) 或容器自动 | LockType.AUTO / REDIS / JDBC |
| 过期时间 | @Locked(expiration=) | 毫秒;Redis 用 PX + SETNX;JDBC 用 expireAt DATETIME |
| 重试总超时 | @Locked(timeout=) | 毫秒;RetryTemplate TimeoutRetryPolicy |
| 重试间隔 | @Locked(retry=) | 毫秒;FixedBackOffPolicy |
| 刷新间隔 | @Locked(refresh=) | 毫秒;框架不内置 watchdog,业务需手动调 Lock.refresh() |
| 存储前缀 | @Locked(storeId=) | Redis:storeId:key;JDBC:storeId 直接当表名 |
| 锁 key | @Locked(expression=) | SpEL 表达式;空字符串取默认 key |
7. 注意事项
| 项 | 说明 |
|---|---|
| SimpleRedisLock 强制单 key | 传多 key 抛 IllegalArgumentException;多 key 用 MultiRedisLock |
| MultiRedisLock 多 key 前缀 | {storeId}.key 大括号 hash tag 保证 Redis Cluster 安全 |
| JDBC 后端锁表必须建 | storeId 参数直接当表名;建表 SQL 见 §3.2 |
| JDBC acquire 事务 | SimpleJdbcLockSingleKeyService 标 @Transactional(READ_COMMITTED + REQUIRES_NEW),每次 acquire 独立事务 |
| RetryableLock null → LockNotAvailableException | acquire 返回 null 不是”异常”,但 RetryTemplate 只能按 Exception 分类重试,所以 RetryableLock 内部把 null 包装成 LockNotAvailableException |
| Token 空值保护 | 空 token("")抛 IllegalStateException,避免”无主锁” |
| 刷新需手动 | 框架不内置 watchdog,长耗时操作需业务显式调 Lock.refresh(keys, storeId, token, expiration) |
| SpEL 失败 | 表达式返回 null 或空 List 抛 EvaluationConvertException |
8. 常见问题
Q: LockType.AUTO 怎么选后端? A: Spring 容器启动时扫 Bean:有 StringRedisTemplate → Redis;否则有 JdbcTemplate → JDBC;都没有 → 报错。
Q: Token 怎么生成? A: 通过构造器传入 Supplier<String>,推荐 UUID。生产环境若需要可追踪 token(如雪花 ID),把 supplier 换成你的实现即可。
Q: Redis 和 JDBC 可以混用吗? A: 不行。Lock 接口三方法语义相同,但底层存储不互通。同一个 storeId 不能既存 Redis 又存 MySQL。
Q: SpEL 表达式怎么写? A: 标准 Spring SpEL,上下文是被调方法的参数(按 @Param 命名)。返回 List<String> 自动多 key;返回 String 自动单 key;null 或空 List 抛 EvaluationConvertException。
Q: AOP 和编程式怎么选? A: 注解驱动简洁、声明式;编程式灵活(可手动 retry 次数、包装条件获取失败)。推荐注解驱动做主干,编程式做复杂场景。
Q: 多 key 锁的原子性? A: MultiRedisLock 用 Lua 脚本遍历 SETNX:全部成功返回 true,任一失败执行全部 DEL 回滚。Redis 单实例原子性保证;Redis Cluster 靠大括号 hash tag 保证多 key 落同一 slot。
Q: allowClockBackward / maxClockBackwardMs 配置不生效? A: 这是 easy-idgen 的已知 bug,不是 easy-lock。