跳到主要内容

分布式锁

Coco 分布式锁(coco-lock)通过 @CocoLock 注解为同步业务方法声明互斥执行。它把"获取锁 → 执行方法 → 释放锁"封装成 AOP 切面:进入方法前按锁键申请租约,方法返回或抛异常后在 finally 中释放,同时后台看门狗(watchdog)在方法执行期间自动为租约续期,避免长任务因租约到期被他人抢占。

锁绑定 coco.lock 命名空间,默认关闭,需显式打开 coco.lock.enabled=true(无 matchIfMissing)。锁不改变事务边界,也不提供 exactly-once 保证。

功能简介

  • 注解式互斥@CocoLock 标注在同步方法或类型上,方法声明覆盖类型声明。
  • SpEL 锁键key 既可以是固定字符串,也可以是 Spring 表达式(#p0#{#order.id} 等),从方法参数动态求值。
  • 有限等待 + 轮询:可配置等待时长与轮询间隔;等待超时未获得锁抛出冲突错误。
  • 同线程可重入:同一线程对同一锁键的嵌套获取会复用已持有的租约,通过重入计数管理,最外层释放时才真正释放锁。
  • 租约与看门狗续期:持锁期间后台线程按锁租约的约 1/3 周期自动续期;续期失败(非本 owner、存储不可用、抛异常)会将该持有标记为"丢失"(lost),后续操作按不可用处理。
  • owner token 保护:续期与释放仅在 owner token 仍匹配时生效,防止误释放他人的锁。
  • SPI 可替换存储CocoLockStore 是原子存储 SPI。默认进程内实现仅适合单实例;集群把 store-type 切到 redis 即用内置的 Lua 原子实现,也可提供自定义 Bean 接入其它存储。

如何启用接入

锁只受单个开关控制:显式打开 coco.lock.enabled。启用后,应用提供的 CocoLockStore Bean 优先于进程内参考实现。

1. 打开开关

coco:
lock:
enabled: true
lease: 30s
wait: 0s
poll-interval: 50ms
watchdog-enabled: true
watchdog-interval: 10s

2. 在方法上声明锁

@Service
public class InventoryService {

// 固定键:整个方法全局互斥
@CocoLock(key = "inventory:rebuild")
public void rebuildIndex() {
// ...
}

// SpEL 键:按订单维度互斥,等待最多 2 秒
@CocoLock(key = "#order.id", waitMillis = 2000)
public void settle(Order order) {
// ...
}
}

key# 开头视为 SpEL 表达式,支持 #{...} 包裹形式;可引用方法参数名、#p0 位置参数以及 #targetleaseMilliswaitMillispollIntervalMillis 为负数时回退到全局配置。锁键为空、无法求值或超过 max-key-length 时抛出无效键错误。

3. 集群部署切换到 Redis 存储

进程内 InMemoryCocoLockStore 的状态只存在于当前 JVM,多实例部署下各实例互不感知,无法实现跨实例互斥;构造时会输出多实例风险警告。

集群环境改用内置的 Redis 存储,只需声明 store-type

coco:
lock:
enabled: true
store-type: redis # 默认 in-memory
redis:
key-prefix: "coco:lock:" # 可选

再引入 Spring Data Redis:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

RedisCocoLockStore 用 Lua 脚本保证获取、续期、释放三步各自原子,且续期与释放都校验 owner token,不会误释放他人的锁。

依赖缺失时明确失败

若设了 store-type: redis 但 classpath 上没有 Spring Data Redis,启动会直接失败并说明原因,不会静默回落到进程内存储。这一点对锁尤其重要——静默回落意味着集群里多个节点同时认为自己持有锁。

多个 StringRedisTemplate Bean 时,用 coco.lock.redis.template-bean-name 显式指定,或把目标 Bean 标记 @Primary;否则启动失败并列出候选,不会随机挑一个。

仍可提供自定义 CocoLockStore Bean 覆盖内置实现(两种 store-type 下都生效):

@Bean
public CocoLockStore cocoLockStore() {
return new MyOwnLockStore(/* ... */);
}

使用示例

错误码

获取失败或运行异常时,切面抛出统一业务码:

业务码常量触发场景
40060INVALID_KEY锁键缺失、无效或表达式无法求值。
40960TIMED_OUT在有限等待时间内未获得锁(竞争)。
50360UNAVAILABLE锁存储不可用,或持锁期间租约丢失。
50060ASYNCHRONOUS_RETURN注解方法返回异步或响应式类型,被拒绝。
50361INTERRUPTED等待锁时线程被中断。

重入示例

@Service
public class ReportService {

@CocoLock(key = "report:daily")
public void generate() {
aggregate(); // 同线程再次进入同键锁,复用租约,不会自阻塞
}

@CocoLock(key = "report:daily")
public void aggregate() {
// ...
}
}

同线程对同键的嵌套调用通过重入计数复用租约,仅在最外层调用返回时释放锁。

关键配置项

前缀 coco.lock

配置项类型默认值说明
enabledbooleanfalse是否启用分布式锁。
leaseDuration30s默认租约时长,可被注解 leaseMillis 覆盖。
waitDuration0s默认获取锁的最大等待时长,0 表示不等待。
poll-intervalDuration50ms等待期间的重试轮询间隔。
watchdog-enabledbooleantrue是否启用后台租约续期看门狗。
watchdog-intervalDuration10s看门狗续期间隔上限(实际周期取该值与租约约 1/3 的较小者)。
max-entriesint100000进程内存储最大活动锁键数。
cleanup-intervalDuration1m过期租约后台清理间隔;为零则关闭后台清理线程。
max-key-lengthint256锁键最大长度,超长视为无效键。
aspect-orderintOrdered.LOWEST_PRECEDENCE - 100锁切面在 AOP 链中的顺序。

边界注意事项

  • 默认关闭:必须显式设置 coco.lock.enabled=true,该属性无 matchIfMissing,未配置即不装配。
  • 进程内存储不可用于集群InMemoryCocoLockStore 仅适合单实例或测试。多实例互斥必须替换为共享 CocoLockStore 实现。
  • 不支持异步/响应式返回:注解方法返回 CompletionStagePublisher 等类型会被直接拒绝(50060),因为切面依赖同步方法边界释放锁。
  • 不提供 exactly-once,也不改变事务:租约可能因看门狗续期失败而丢失(网络分区、存储抖动等),此时持有会被标记 lost。业务需自行处理临界区被抢占后的一致性,锁不等于事务。
  • 句柄由获取线程释放:底层持有句柄要求由获取它的同一线程关闭,跨线程释放会抛出状态异常。
  • 续期依赖存储可用性:看门狗续期失败即视为丢失锁;对强一致要求高的场景应结合业务幂等与冲突检测。