分布式锁
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 位置参数以及 #target。leaseMillis、waitMillis、pollIntervalMillis 为负数时回退到全局配置。锁键为空、无法求值或超过 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(/* ... */);
}
使用示例
错误码
获取失败或运行异常时,切面抛出统一业务码:
| 业务码 | 常量 | 触发场景 |
|---|---|---|
40060 | INVALID_KEY | 锁键缺失、无效或表达式无法求值。 |
40960 | TIMED_OUT | 在有限等待时间内未获得锁(竞争)。 |
50360 | UNAVAILABLE | 锁存储不可用,或持锁期间租约丢失。 |
50060 | ASYNCHRONOUS_RETURN | 注解方法返回异步或响应式类型,被拒绝。 |
50361 | INTERRUPTED | 等待锁时线程被中断。 |
重入示例
@Service
public class ReportService {
@CocoLock(key = "report:daily")
public void generate() {
aggregate(); // 同线程再次进入同键锁,复用租约,不会自阻塞
}
@CocoLock(key = "report:daily")
public void aggregate() {
// ...
}
}
同线程对同键的嵌套调用通过重入计数复用租约,仅在最外层调用返回时释放锁。
关键配置项
前缀 coco.lock。
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled | boolean | false | 是否启用分布式锁。 |
lease | Duration | 30s | 默认租约时长,可被注解 leaseMillis 覆盖。 |
wait | Duration | 0s | 默认获取锁的最大等待时长,0 表示不等待。 |
poll-interval | Duration | 50ms | 等待期间的重试轮询间隔。 |
watchdog-enabled | boolean | true | 是否启用后台租约续期看门狗。 |
watchdog-interval | Duration | 10s | 看门狗续期间隔上限(实际周期取该值与租约约 1/3 的较小者)。 |
max-entries | int | 100000 | 进程内存储最大活动锁键数。 |
cleanup-interval | Duration | 1m | 过期租约后台清理间隔;为零则关闭后台清理线程。 |
max-key-length | int | 256 | 锁键最大长度,超长视为无效键。 |
aspect-order | int | Ordered.LOWEST_PRECEDENCE - 100 | 锁切面在 AOP 链中的顺序。 |
边界注意事项
- 默认关闭:必须显式设置
coco.lock.enabled=true,该属性无matchIfMissing,未配置即不装配。 - 进程内存储不可用于集群:
InMemoryCocoLockStore仅适合单实例或测试。多实例互斥必须替换为共享CocoLockStore实现。 - 不支持异步/响应式返回:注解方法返回
CompletionStage、Publisher等类型会被直接拒绝(50060),因为切面依赖同步方法边界释放锁。 - 不提供 exactly-once,也不改变事务:租约可能因看门狗续期失败而丢失(网络分区、存储抖动等),此时持有会被标记 lost。业务需自行处理临界区被抢占后的一致性,锁不等于事务。
- 句柄由获取线程释放:底层持有句柄要求由获取它的同一线程关闭,跨线程释放会抛出状态异常。
- 续期依赖存储可用性:看门狗续期失败即视为丢失锁;对强一致要求高的场景应结合业务幂等与冲突检测。