一佳技术

easy-lock 分布式锁组件设计

本文档描述 easy-lock 组件的设计与实现。该组件提供注解驱动 + 编程式两套 API,底层支持 Redis(单 key / 多 key)、JDBC(数据库单 key) 两种后端,并内置重试、间隔转换、SpEL key 生成、锁类型自动解析四个增强能力。核心接口 Lock 抽象 acquire/release/refresh 三操作,生产代码零外部 MQ 依赖。


一、背景与问题

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 混用某些场景需要锁住整组 keySimpleRedisLock 单 key / MultiRedisLock 多 key(Redis Cluster hash tag)

1.2 与现有方案的对比

方案覆盖度特点
RedissonRedis only功能全但重,依赖高
ShedLockJDBC 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

分层:

  1. 接口层:Lock 定义三操作
  2. 实现层:三种后端(Redis 单/多 + JDBC 单)直接实现 Lock
  3. 抽象层:AbstractSimpleLock 对单 key 场景公共断言
  4. 装饰层:RetryableLock 包装任意 Lock 加重试逻辑
  5. 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 天然支持多 key
  • storeId:存储维度。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
类职责文件
Lockacquire/release/refresh 三操作接口Lock.java
SimpleRedisLockSETNX + Lua 脚本实现(单 key)SimpleRedisLock.java
MultiRedisLock大括号 hash tag Lua 脚本(多 key,Redis Cluster 友好)MultiRedisLock.java
SimpleJdbcLock + SimpleJdbcLockSingleKeyServiceJDBC 单 key:DELETE EXPIRED → INSERT → DuplicateKeyException 捕获 → 返回 null/trueSimpleJdbcLockSingleKeyService.java
RetryableLock装饰器:null → LockNotAvailableException → RetryTemplateRetryableLock.java
SpelKeyGeneratorSPEL 表达式 → List<String>,null/空抛 EvaluationConvertExceptionSpelKeyGenerator.java
EasyLockConfiguration@Configuration,按 LockType AUTO 自动装配 SimpleRedisLock / SimpleJdbcLockEasyLockConfiguration.java

依赖:

依赖scope说明
spring-boot-starter-aopcompileAOP 拦截
spring-boot-starter-data-redisoptionalRedisTemplate
spring-boot-starter-jdbcoptionalJdbcTemplate
spring-retrycompileRetryTemplate
io.matrix:kernel-commoncompileJsonMapper / 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 流程:
    1. DELETE FROM storeId WHERE expireAt < NOW() — 清理过期
    2. INSERT INTO storeId (lock_key, token, expireAt) VALUES (?, ?, ?)
    3. DuplicateKeyException → 返回 null(被别人持有)
    4. 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 组装:

组件配置
TimeoutRetryPolicytimeout = intervalConverter.toMillis(locked.timeout())
SimpleRetryPolicyInteger.MAX_VALUE 次,只重试 LockNotAvailableException
FixedBackOffPolicybackOffPeriod = 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 转 LockNotAvailableExceptionRetryTemplate 按 Exception 分类重试acquire 返回 null 属于”可重试失败”,必须包装成异常才能被 SimpleRetryPolicy 匹配
JDBC 实现用 @Transactional(READ_COMMITTED + REQUIRES_NEW)避免脏读 + 每次 acquire 独立事务过期清理必须和 INSERT 同事务,失败则全部回滚
Redis Cluster 多 key 用大括号 hash tag{storeId}.key1 和 {storeId}.key2 强制落同一 hash slotMultiRedisLock 的 Lua 多 key 原子性在 Cluster 下才安全
生产代码不提供 watchdog自动续期需要后台线程 + 心跳协议,复杂度高refresh() 显式续期,业务层控制节奏;避免框架线程泄漏
SpelKeyGenerator null/空值抛 EvaluationConvertException动态 key 生成失败属于配置错误,fail-fast不能用默认 key 兜底(会导致不同业务场景拿到同一个锁)

十一、测试覆盖

当前共 43 例(全绿):

测试类用例覆盖点
AbstractSimpleLockTest5单 key ok / 多 key 抛异常 / 空 token 抛异常(用匿名子类)
BeanFactoryAwareIntervalConverterTest6MS / SECONDS / MINUTES unit 转换、null/empty/非数字 resolveEmbeddedValue 边界
RetryableLockTest5首次成功 / 前 N-1 次 null 重试后成功 / 耗尽重试抛 LockNotAvailableException / release/refresh 不重试
DefaultRetryTemplateConverterTest2正常构建 RetryTemplate、零值构建
SimpleRedisLockTest6acquire 成功返回 token / execute 返回 false → null / 空 token 抛异常 / 多 key 抛异常 / release/refresh
MultiRedisLockTest5多 key 前缀 {storeId}.key / 空 token / execute 返回 false → null / release/refresh
SimpleJdbcLockTest4委托 JdbcLockSingleKeyService / 多 key 抛异常
SimpleJdbcLockSingleKeyServiceTest7acquire 正常 / DuplicateKeyException → null / update=0 → null / release 成功失败 / refresh 成功失败
LockExceptionTest3LockNotAvailableException 层级 / EvaluationConvertException / DistributedLockException 两构造器

可测程度 3/5:核心类用 Mockito mock StringRedisTemplate + mock JdbcTemplate 隔离外部依赖。AOP 层(LockMethodInterceptor / LockBeanPostProcessor / SpelKeyGenerator / EasyLockConfiguration)属 Spring 集成测试范畴,当前未覆盖。