easy-lock 分布式锁组件设计
本文档描述 easy-lock 组件的设计与实现。该组件提供注解驱动 + 编程式两套 API,底层支持 Redis(单 key / 多 key)、JDBC(数据库单 key) 两种后端,并内置重试、间隔转换、SpEL key 生成、锁类型自动解析四个增强能力。核心接口 Lock 抽象 acquire/release/refresh 三操作,生产代码零外部 MQ 依赖。
- 一、背景与问题
- 二、组件定位与边界
- 三、总体架构
- 四、核心 API 草图
- 五、包结构与核心类
- 六、三种后端实现
- 七、AOP 注解处理流程
- 八、重试与间隔
- 九、使用示例
- 十、关键设计决策
- 十一、测试覆盖
一、背景与问题
1.1 常见痛点
| 痛点 | 现状 | 目标 |
|---|---|---|
| 多后端不统一 | Redis 用 SETNX、JDBC 用 INSERT EXCEPT,API 完全不同 | Lock 接口抽象 acquire/release/refresh,后端可替换 |
| 超时/重试样板 | 每个业务自己写 while + sleep 轮询 | RetryableLock 装饰器 + RetryTemplate 统一处理 |
| key 动态拼 | "order:" + orderId 硬编码在各处 | SpelKeyGenerator 统一生成,注解里写表达式 |
| 自动续期缺失 | 拿到锁后业务长耗时,锁过期被抢 | Lock.refresh() 显式续期(框架层不做 watchdog,避免复杂度) |
| 单 key 多 key 混用 | 某些场景需要锁住整组 key | SimpleRedisLock 单 key / MultiRedisLock 多 key(Redis Cluster hash tag) |
1.2 与现有方案的对比
| 方案 | 覆盖度 | 特点 |
|---|---|---|
| Redisson | Redis only | 功能全但重,依赖高 |
| ShedLock | JDBC only | 定时任务专用,无 API |
自研 SETNX + lua | 灵活但重复造轮子 | easy-lock 把 Redis / JDBC 两种后端统一到 Lock 接口,内置重试 + AOP |
二、组件定位与边界
定位:后端中立的分布式锁门面。业务方声明 Locked 注解或注入 Lock Bean,即可获得 acquire/release/refresh 能力,可插拔 Redis/JDBC 后端 + 可选重试。
边界:
| 做 | 不做 |
|---|---|
| acquire / release / refresh 三操作 | 不做”看门狗自动续期”(业务需要时调 refresh) |
| SpEL 表达式 key 生成 | 不做公平锁(Redis 用 SETNX 天然非公平) |
| 重试装饰器(RetryableLock) | 不做分布式可重入(token 机制天然解决所有权,但重入需业务层保证) |
| 单 key / 多 key Redis 实现 + JDBC 单 key 实现 | 不做 ZooKeeper / Etcd / Curator(optional scope 可补) |
| Locked / Interval 注解 + AOP 拦截 | 不做 Spring Boot 自动配置外的容器外运行 |
约束:
AbstractSimpleLock对多 key 调用抛IllegalArgumentException(只有 MultiRedisLock 天然支持多 key)- token 为 null 或空抛
IllegalStateException - Redis 实现依赖
StringRedisTemplate,RedisTemplate 为 optional scope 依赖
三、总体架构
flowchart TD
subgraph "业务层"
A1["@Locked(expression='#orderId', retry=@Interval(...))<br/>注解驱动"]
A2["Lock.acquire(keys, storeId, expiration)<br/>编程式"]
end
subgraph "AOP 层 (advice/)"
B1["LockMethodInterceptor<br/>Around advice"]
B2["LockBeanPostProcessor<br/>注册 LockMethodInterceptor"]
B3["LockTypeResolver<br/>按 Lock.type() 查找 Lock Bean"]
end
subgraph "key 生成 (key/)"
C1["SpelKeyGenerator<br/>SpEL 表达式 → List<String>"]
end
subgraph "公共抽象"
D1["Lock 接口<br/>acquire / release / refresh"]
D2["AbstractSimpleLock<br/>单 key 断言 + token 非空断言"]
end
subgraph "后端实现"
E1["redis/SimpleRedisLock<br/>SETNX + Lua 3 段脚本"]
E2["redis/MultiRedisLock<br/>大括号 hash tag Lua 脚本"]
E3["jdbc/SimpleJdbcLock → JdbcLockSingleKeyService<br/>DELETE EXPIRED + INSERT + DuplicateKeyException"]
end
subgraph "重试层 (retry/)"
F1["RetryableLock<br/>装饰器:null → LockNotAvailableException → retry"]
F2["DefaultRetryTemplateConverter<br/>Composite: TimeoutRetry + SimpleRetry + FixedBackOff"]
end
subgraph "间隔层 (interval/)"
G1["Interval 注解 (value, unit)"]
G2["BeanFactoryAwareIntervalConverter<br/>SpEL + StringUtils + Long.parseLong + unit.toMillis"]
end
A1 --> B1 --> B3 --> D1
A2 --> D1
B1 --> C1
D1 --> E1
D1 --> E2
D1 --> E3
D2 --> E1
D2 --> E3
E2 -- "implements Lock" --> D1
F1 -->|"包装"| D1
F1 --> F2
F2 --> G2
B1 --> F1
分层:
- 接口层:
Lock定义三操作 - 实现层:三种后端(Redis 单/多 + JDBC 单)直接实现 Lock
- 抽象层:
AbstractSimpleLock对单 key 场景公共断言 - 装饰层:
RetryableLock包装任意 Lock 加重试逻辑 - AOP 层:注解 → BeanPostProcessor → MethodInterceptor → 解析类型 + 生成 key → acquire + 执行 → release
四、核心 API 草图
4.1 Lock 接口
public interface Lock {
/** 尝试获取锁,返回 token(null 表示未获取到) */
String acquire(List<String> keys, String storeId, long expiration);
/** 释放锁,true 表示成功释放 */
boolean release(List<String> keys, String storeId, String token);
/** 刷新锁过期时间 */
boolean refresh(List<String> keys, String storeId, String token, long expiration);
}
keys:锁标识列表。SimpleRedisLock/JDBC 实现必须单 key(AbstractSimpleLock 断言);MultiRedisLock 天然支持多 keystoreId:存储维度。Redis 用作 key 前缀(storeId:key);JDBC 用作表名expiration:过期时间毫秒- 返回值:
acquire返回 token(唯一标识,释放/续期必须用);release/refresh返回 boolean
4.2 注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)
public @interface Locked {
boolean manuallyReleased() default false; // false=方法结束自动释放
String storeId() default "lock"; // 存储维度
String prefix() default ""; // key 前缀
String expression() default ""; // SpEL 表达式 → List<String>
long expiration() default 60000L; // 过期毫秒
long timeout() default 30000L; // 重试总超时毫秒
long retry() default 100L; // 重试间隔毫秒
long refresh() default 10000L; // 刷新间隔毫秒
LockType type() default LockType.AUTO; // REDIS / JDBC / AUTO(按容器类型)
}
4.3 RetryableLock 装饰器
// acquire 返回 null → 抛 LockNotAvailableException → RetryTemplate 重试
RetryTemplate rt = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(100)
.retryOn(LockNotAvailableException.class)
.build();
Lock retryable = new RetryableLock(redisLock, rt);
String token = retryable.acquire(keys, store, 5000); // null → 重试 3 次后抛
五、包结构与核心类
easy-lock/
└── io/matrix/lock/
├── Lock.java # 核心接口
├── Locked.java # 注解
├── Interval.java # 注解 (value + unit)
├── AbstractSimpleLock.java # 单 key 断言 + token 非空断言
├── redis/
│ ├── alias/RedisLocked.java
│ └── impl/
│ ├── SimpleRedisLock.java # 单 key Redis(SETNX + Lua)
│ └── MultiRedisLock.java # 多 key Redis(hash tag Lua)
├── jdbc/
│ ├── alias/JdbcLocked.java
│ ├── impl/SimpleJdbcLock.java
│ └── service/
│ ├── JdbcLockSingleKeyService.java # 接口
│ └── SimpleJdbcLockSingleKeyService.java # @Transactional + JdbcTemplate 实现
├── retry/
│ ├── RetryableLock.java # 装饰器
│ ├── RetryableLockFactory.java # SPI
│ ├── DefaultRetryableLockFactory.java # 默认实现
│ ├── RetryTemplateConverter.java # SPI
│ └── DefaultRetryTemplateConverter.java # 默认实现(TimeOut + Simple + FixedBackOff)
├── interval/
│ ├── IntervalConverter.java # SPI
│ └── BeanFactoryAwareIntervalConverter.java # SpEL + unit.toMillis()
├── key/
│ ├── KeyGenerator.java
│ └── SpelKeyGenerator.java
├── advice/
│ ├── LockMethodInterceptor.java
│ ├── LockBeanPostProcessor.java
│ └── LockTypeResolver.java
├── autoconfig/EasyLockConfiguration.java
└── exception/
├── DistributedLockException.java
├── LockNotAvailableException.java
└── EvaluationConvertException.java
| 类 | 职责 | 文件 |
|---|---|---|
| Lock | acquire/release/refresh 三操作接口 | Lock.java |
| SimpleRedisLock | SETNX + Lua 脚本实现(单 key) | SimpleRedisLock.java |
| MultiRedisLock | 大括号 hash tag Lua 脚本(多 key,Redis Cluster 友好) | MultiRedisLock.java |
| SimpleJdbcLock + SimpleJdbcLockSingleKeyService | JDBC 单 key:DELETE EXPIRED → INSERT → DuplicateKeyException 捕获 → 返回 null/true | SimpleJdbcLockSingleKeyService.java |
| RetryableLock | 装饰器:null → LockNotAvailableException → RetryTemplate | RetryableLock.java |
| SpelKeyGenerator | SPEL 表达式 → List<String>,null/空抛 EvaluationConvertException | SpelKeyGenerator.java |
| EasyLockConfiguration | @Configuration,按 LockType AUTO 自动装配 SimpleRedisLock / SimpleJdbcLock | EasyLockConfiguration.java |
依赖:
| 依赖 | scope | 说明 |
|---|---|---|
spring-boot-starter-aop | compile | AOP 拦截 |
spring-boot-starter-data-redis | optional | RedisTemplate |
spring-boot-starter-jdbc | optional | JdbcTemplate |
spring-retry | compile | RetryTemplate |
io.matrix:kernel-common | compile | JsonMapper / Exception |
六、三种后端实现
6.1 SimpleRedisLock(单 key)
- key 格式:
storeId + ":" + key - Lua 脚本:
SET key token NX PX expiration+ 校验 token +DEL key/PEXPIRE key expiration - 构造器:
(Supplier<String> tokenSupplier, StringRedisTemplate)
6.2 MultiRedisLock(多 key)
- key 格式:
{storeId}.key(大括号 hash tag,Redis Cluster 下多 key 操作安全) - Lua 脚本:遍历 key 依次
SET ... NX PX ...;全部成功返回true,任一失败全部 DEL 回滚 - 构造器:
(StringRedisTemplate)或(StringRedisTemplate, Supplier<String>)
6.3 SimpleJdbcLock(单 key)
- 委托
JdbcLockSingleKeyService(@Transactional + REQUIRES_NEW) - acquire 流程:
DELETE FROM storeId WHERE expireAt < NOW()— 清理过期INSERT INTO storeId (lock_key, token, expireAt) VALUES (?, ?, ?)DuplicateKeyException→ 返回 null(被别人持有)update == 1→ 返回 token(持有)
- release:
DELETE WHERE lock_key = ? AND token = ? - refresh:
UPDATE SET expireAt = ? WHERE lock_key = ? AND token = ?
七、AOP 注解处理流程
flowchart TD
A["@Locked(expression='#orderId', retry=100) on Service.method()"]
B["LockMethodInterceptor<br/>@Around"]
C["SpelKeyGenerator<br/>执行 SPEL → List<String>"]
D["LockTypeResolver<br/>按 type=AUTO 扫描容器找 Lock Bean"]
E["Lock.acquire(keys, storeId, expiration)<br/>可选:包装 RetryableLock"]
F["proceed() — 执行业务方法"]
G{manuallyReleased?}
H["Lock.release(keys, storeId, token)"]
I["方法返回"]
A --> B
B --> C
B --> D
C --> E
D --> E
E --> F
F --> G
G -- "false 默认" --> H --> I
G -- "true" --> I
八、重试与间隔
// Interval 注解
public @interface Interval {
String value(); // 默认 100
TimeUnit unit() default TimeUnit.MILLISECONDS;
}
// IntervalConverter SPI:把 Interval → long 毫秒
public interface IntervalConverter { long toMillis(Interval interval); }
// BeanFactoryAwareIntervalConverter:支持 ${spEL} 占位符
// resolveEmbeddedValue → StringUtils.hasText → Long.parseLong → unit.toMillis
DefaultRetryTemplateConverter 组装:
| 组件 | 配置 |
|---|---|
| TimeoutRetryPolicy | timeout = intervalConverter.toMillis(locked.timeout()) |
| SimpleRetryPolicy | Integer.MAX_VALUE 次,只重试 LockNotAvailableException |
| FixedBackOffPolicy | backOffPeriod = intervalConverter.toMillis(locked.retry()) |
RetryTemplate.setThrowLastExceptionOnExhausted(true) —— 耗尽后抛 LockNotAvailableException。
九、使用示例
9.1 编程式 + Redis
StringRedisTemplate redis = ...;
SimpleRedisLock lock = new SimpleRedisLock(() -> UUID.randomUUID().toString(), redis);
String token = lock.acquire(List.of("order-123"), "order-lock", 60000);
if (token != null) {
try {
// 业务逻辑
lock.refresh(List.of("order-123"), "order-lock", token, 60000); // 长耗时续期
} finally {
lock.release(List.of("order-123"), "order-lock", token);
}
}
9.2 注解驱动 + 自动重试
@Service
public class OrderService {
@Locked(
expression = "#order.id",
storeId = "order-lock",
expiration = 60000L,
timeout = 30000L, // 总超时
retry = 100L, // 100ms 间隔重试
type = LockType.REDIS
)
public void pay(Order order) { ... }
}
9.3 多 key(Redis Cluster 跨 hash)
MultiRedisLock lock = new MultiRedisLock(redis);
String token = lock.acquire(Arrays.asList("account-A", "account-B"), "account", 30000);
// Lua 脚本确保要么全部成功要么全部回滚
十、关键设计决策
| 决策 | 内容 | 理由 |
|---|---|---|
Lock 接口用 List<String> 而非单一 String | 统一抽象:单 key 场景给 List.of(),多 key 给 Arrays.asList() | 避免接口分叉成 SimpleLock / MultiLock |
| AbstractSimpleLock 对多 key 抛异常 | 只有 MultiRedisLock 天然支持多 key,单 key 实现无法安全处理 | fail-fast,错误在构建期暴露 |
| token supplier 构造器注入 | 让 Lock 实例复用,每次 acquire 生成新 token(UUID / 雪花 / 自增均可) | 避免 Lock 实例持有随机状态;测试可注入固定 supplier 断言 |
| RetryableLock 把 null 转 LockNotAvailableException | RetryTemplate 按 Exception 分类重试 | acquire 返回 null 属于”可重试失败”,必须包装成异常才能被 SimpleRetryPolicy 匹配 |
| JDBC 实现用 @Transactional(READ_COMMITTED + REQUIRES_NEW) | 避免脏读 + 每次 acquire 独立事务 | 过期清理必须和 INSERT 同事务,失败则全部回滚 |
| Redis Cluster 多 key 用大括号 hash tag | {storeId}.key1 和 {storeId}.key2 强制落同一 hash slot | MultiRedisLock 的 Lua 多 key 原子性在 Cluster 下才安全 |
| 生产代码不提供 watchdog | 自动续期需要后台线程 + 心跳协议,复杂度高 | refresh() 显式续期,业务层控制节奏;避免框架线程泄漏 |
| SpelKeyGenerator null/空值抛 EvaluationConvertException | 动态 key 生成失败属于配置错误,fail-fast | 不能用默认 key 兜底(会导致不同业务场景拿到同一个锁) |
十一、测试覆盖
当前共 43 例(全绿):
| 测试类 | 用例 | 覆盖点 |
|---|---|---|
| AbstractSimpleLockTest | 5 | 单 key ok / 多 key 抛异常 / 空 token 抛异常(用匿名子类) |
| BeanFactoryAwareIntervalConverterTest | 6 | MS / SECONDS / MINUTES unit 转换、null/empty/非数字 resolveEmbeddedValue 边界 |
| RetryableLockTest | 5 | 首次成功 / 前 N-1 次 null 重试后成功 / 耗尽重试抛 LockNotAvailableException / release/refresh 不重试 |
| DefaultRetryTemplateConverterTest | 2 | 正常构建 RetryTemplate、零值构建 |
| SimpleRedisLockTest | 6 | acquire 成功返回 token / execute 返回 false → null / 空 token 抛异常 / 多 key 抛异常 / release/refresh |
| MultiRedisLockTest | 5 | 多 key 前缀 {storeId}.key / 空 token / execute 返回 false → null / release/refresh |
| SimpleJdbcLockTest | 4 | 委托 JdbcLockSingleKeyService / 多 key 抛异常 |
| SimpleJdbcLockSingleKeyServiceTest | 7 | acquire 正常 / DuplicateKeyException → null / update=0 → null / release 成功失败 / refresh 成功失败 |
| LockExceptionTest | 3 | LockNotAvailableException 层级 / EvaluationConvertException / DistributedLockException 两构造器 |
可测程度 3/5:核心类用 Mockito mock StringRedisTemplate + mock JdbcTemplate 隔离外部依赖。AOP 层(LockMethodInterceptor / LockBeanPostProcessor / SpelKeyGenerator / EasyLockConfiguration)属 Spring 集成测试范畴,当前未覆盖。