一佳技术

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 用作表名
expiration60000L锁过期毫秒
timeout30000L重试总超时毫秒(Spring Retry TimeoutRetryPolicy)
retry100L重试间隔毫秒
refresh10000L刷新间隔毫秒(框架不内置 watchdog,refresh 需业务显式调)
prefix""key 前缀(追加到 expression 结果之前)
typeLockType.AUTO后端选择(AUTO/REDIS/JDBC)
manuallyReleasedfalsefalse=方法结束自动释放;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 → LockNotAvailableExceptionacquire 返回 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。