首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >我用注解把分布式锁‘卷’到了新高度:支持双栈、自旋策略,已发Central!

我用注解把分布式锁‘卷’到了新高度:支持双栈、自旋策略,已发Central!

作者头像
烟雨平生
发布2026-04-14 18:33:26
发布2026-04-14 18:33:26
320
举报

“重点是大模型的应用,知识不等于能力”,然后就用大模型写一个分布式锁的组件,写的又快又好,分享一下。

下面来分享下从零到发布:Spring声明式分布式锁的设计、实现与Central发布全记录。

一个注解,兼顾 Spring Boot 2.x 与 Spring Boot 3.X;一套代码,从设计到 Central 发布。附完整可复用的自旋策略、安全释放与发布 SOP。

在高并发、分布式业务中,「锁」是一个无法绕开的主题。但你是否也曾纠结于:

  • 每个锁的获取与释放都要写样板代码
  • 锁的 Key 拼凑逻辑分散在各处,难以维护
  • 项目从 Spring Boot 2 升级到 3,原有的锁组件不兼容
  • 好不容易写好一个组件,却不知道如何发布到 Maven Central 供他人使用

针对这些问题,设计并实现了一个 Spring 注解式分布式锁组件,它不仅解决了上述痛点,还成功发布到了 Maven Central。本文将与你分享从设计、实现、测试到发布的完整过程,包含可直接复用的代码示例和详细的发布避坑指南。

一、为什么需要这个组件?

1. 声明式开发体验 在业务中,我们频繁遇到“幂等性处理”或“并发下保护关键资源”的需求。最自然的开发方式是在方法级别声明锁规则,而不是在业务代码中嵌入一堆 tryLockunlock 的样板代码。

2. 灵活的锁 Key 表达 锁的粒度往往由业务参数决定。我们需要一个既能支持 SpEL 动态表达式,又能从方法参数或 DTO 对象的字段注解中自动抽取关键值来组装锁 Key 的方案。

3. 生态兼容性要求 很多团队仍在使用 Spring Boot 2.x,但新项目已转向 Boot 3.x。我们希望一个组件能同时兼容两者,避免维护两套代码或给业务方带来迁移负担。

基于这些目标,组件被设计为具有以下核心特性:

  • 方法注解 @Lock:定义锁的前缀、分隔符、过期时间、等待策略等。
  • Union All 锁 Key@LockKeyParam 注解的片段与 SpEL 片段按固定顺序拼接,语义清晰。
  • 可配置的自旋策略:支持固定间隔、线性递增、指数退避等多种等待策略。
  • 安全释放:基于随机令牌和 Lua 脚本的原子删除,防止误解锁。
  • 无缝自动装配:同时支持 Spring Boot 2 与 3,无需任何 @Enable* 注解,引入依赖即生效。

二、快速开始:5分钟上手

第1步:添加依赖

根据你的构建工具选择其一。

Maven

代码语言:javascript
复制
<dependency>
  <groupId>io.github.helloworldtang</groupId>
  <artifactId>lock-key-param</artifactId>
  <version>0.1.0</version>
</dependency>
<dependency>
  <groupId>io.github.helloworldtang</groupId>
  <artifactId>distributed-lock-redis-spring</artifactId>
  <version>0.1.0</version>
</dependency>
代码语言:javascript
复制
Gradle (Groovy)
代码语言:javascript
复制
dependencies {
  implementation 'io.github.helloworldtang:lock-key-param:0.1.0'
  implementation 'io.github.helloworldtang:distributed-lock-redis-spring:0.1.0'
}
代码语言:javascript
复制
Gradle (Kotlin)
代码语言:javascript
复制
dependencies {
  implementation("io.github.helloworldtang:lock-key-param:0.1.0")
  implementation("io.github.helloworldtang:distributed-lock-redis-spring:0.1.0")
}
代码语言:javascript
复制
第2步:配置 Redis

确保你的 application.yml 中包含 Redis 连接信息。

代码语言:javascript
复制
# application.yml
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
代码语言:javascript
复制
第3步:定义 DTO 并标注锁键

在需要作为锁标识的字段上添加 @LockKeyParam 注解。

代码语言:javascript
复制
package com.example.dto;

import com.github.chengtang.lockkey.LockKeyParam;

public class OrderRequest {
  @LockKeyParam
  private Long userId;

  @LockKeyParam
  private Long orderId;

  public Long getUserId() { return userId; }
  public void setUserId(Long userId) { this.userId = userId; }
  public Long getOrderId() { return orderId; }
  public void setOrderId(Long orderId) { this.orderId = orderId; }
}
代码语言:javascript
复制
第4步:在 Service 方法上使用 @Lock

组件支持两种模式:快速失败自旋等待

代码语言:javascript
复制
package com.example.service;

import com.github.chengtang.dlock.annotation.Lock;
import com.github.chengtang.dlock.annotation.SpinWaitStrategy;
import com.github.chengtang.dlock.annotation.SpinWaitTimeParam;
import com.github.chengtang.lockkey.LockKeyParam;
import com.example.dto.OrderRequest;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class OrderService {
  private final AtomicInteger calls = new AtomicInteger();

  // 快速失败:waitTime=0
  @Lock(prefix = "dl", delimiter = ":", expireTime = 5, waitTime = 0, timeUnit = TimeUnit.SECONDS)
  public int placeFastFail(OrderRequest req, @LockKeyParam Long orderId) {
    try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    return calls.incrementAndGet();
  }

  // 等待窗口:waitTime=2s,线性自旋 interval=100ms
  @Lock(
    prefix = "dl", delimiter = ":", expireTime = 5, waitTime = 2, timeUnit = TimeUnit.SECONDS,
    spinWaitTimeParam = @SpinWaitTimeParam(interval = 100, maxAttempts = 0, strategy = SpinWaitStrategy.LINEAR)
  )
  public int placeWait(OrderRequest req, @LockKeyParam Long orderId) {
    try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    return calls.incrementAndGet();
  }
}

第5步:暴露 API 接口

代码语言:javascript
复制
package com.example.web;

import com.example.dto.OrderRequest;
import com.example.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
  private final OrderService service;
  public OrderController(OrderService service) { this.service = service; }

  @PostMapping("/orders/place-fast")
  public int placeFast(@RequestBody OrderRequest req) {
    return service.placeFastFail(req, req.getOrderId());
  }

  @PostMapping("/orders/place-wait")
  public int placeWait(@RequestBody OrderRequest req) {
    return service.placeWait(req, req.getOrderId());
  }
}
代码语言:javascript
复制
第6步:运行与验证

启动应用后,可以使用 curl、Postman 或编写简单的并发测试进行验证。

快速失败场景:并发发送两个相同 userIdorderId 的请求到 /orders/place-fast。预期结果:一个成功(200),另一个因未获取到锁而快速失败(500或自定义异常)。

自旋等待场景:并发发送两个相同请求到 /orders/place-wait。预期结果:两个都成功(200),但第二个请求的响应时间会略有增加(约 0.7s ~ 2s),因为它需要等待第一个请求释放锁。

三、核心设计揭秘

1. 整体架构流程图

2. 锁的获取与释放时序图

3. 关键设计决策

锁 Key 的 Union All 策略 锁 Key 由两部分按固定顺序拼接而成:

  1. 注解片段:来自方法参数上的 @LockKeyParam 或 DTO 字段上的 @LockKeyParam
  2. SpEL :来自 @Lock 注解中的 keys 属性,支持完整的 Spring 表达式语言。

这种“先注解,后 SpEL”的顺序保证了 Key 组成的可预测性和可读性。

线程安全的令牌管理 每个锁在获取时都会生成一个全局唯一的 UUID 作为令牌。该令牌不仅会存入 Redis,还会保存在调用线程的 ThreadLocal 中。在释放锁时,会通过 Lua 脚本原子性地比较并删除,确保“只有锁的持有者才能释放锁”,杜绝了误解锁的风险。

单包双栈兼容 这是支持 Spring Boot 2.x 和 3.x 的关键。实现原理是:

  • 对于 Boot 3:在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中声明自动配置类。
  • 对于 Boot 2:在 META-INF/spring.factories 文件中声明自动配置类。 组件通过条件化装配(@ConditionalOnClass 等)确保只在存在 StringRedisTemplate 等必要 Bean 时才生效。

可扩展的自旋策略 内置了三种等待策略,可通过 SpinWaitStrategy 指定:

  • FIXED:固定间隔重试。
  • LINEAR:每次重试间隔线性递增。
  • EXPONENTIAL:每次重试间隔指数退避(避免惊群效应)。

四、发布到 Maven Central 全攻略

将组件发布到中央仓库,使其能被全球开发者直接引用,是项目成熟的重要标志。以下是经过实战验证的 SOP(标准作业程序)和常见坑点。

发布前的准备

  1. 注册 Sonatype JIRA 账号 访问 issues.sonatype.org,注册账号并创建一个新的项目发布请求(Project Ticket)。通常,以 io.github.你的用户名 作为 GroupId 的请求会很快被批准。 Tips:可以使用github账号登录sonatype。
  1. 安装并配置 GPG 组件发布必须经过 GPG 签名。 # macOS 安装 brew install gnupg # 生成密钥对(推荐 RSA 4096) gpg --full-generate-key # 将公钥发送到密钥服务器 gpg --keyserver keyserver.ubuntu.com --send-keys <你的密钥ID>

没有gpg签名,会报上面的错“Missing signature”

  1. 配置 Maven settings.xml

~/.m2/settings.xml 中配置 Sonatype 的账号和 GPG 密码。

https://central.sonatype.com/publishing/deployments

图中的username和password,会在下面的settings.xml文件用到

代码语言:javascript
复制
<settings>
    <servers>
        <server>
            <id>central</id>
            <username>你的Sonatype用户名</username>
            <password>你的Sonatype密码(建议使用Token)</password>
        </server>
    </servers>
    <profiles>
        <profile>
            <id>gpg</id>
            <properties>
                <gpg.executable>gpg</gpg.executable>
                <gpg.passphrase>你的GPG私钥密码</gpg.passphrase>
            </properties>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>gpg</activeProfile>
    </activeProfiles>
</settings>

发布执行命令

项目父POM需要集成 central-publishing-maven-plugin 并配置 release 环境。发布时,通常只发布核心库模块,跳过示例模块。

代码语言:javascript
复制
mvn -s /path/to/settings.xml \
    -Dmaven.repo.local=~/.m2/repository \
    -Prelease -DskipTests \
    -pl lock-key-param,distributed-lock-redis-spring -am deploy
代码语言:javascript
复制
参数说明:
  • -Prelease:激活发布配置。
  • -pl:指定要部署的模块列表。
  • -am:同时构建这些模块所依赖的模块。

高频“踩坑”点及解决方案

  1. Namespace not allowed 问题:部署失败,提示 GroupId 未授权。 解决:确保 pom.xml 中的 groupId 与你在 Sonatype JIRA 上申请并获批的命名空间 完全一致(例如 io.github.helloworldtang)。
  2. Missing signature 问题:组件缺少 GPG 签名。 解决
    • 确认 settings.xml 中 GPG 配置正确。
    • 确保 maven-gpg-plugin 插件在 release 流程中被正确执行。
    • 本地测试签名:mvn verify -Prelease
  3. 409 Conflict 或构件已存在 问题:相同版本的构件已经发布到 Central,无法覆盖。 解决:Maven Central 不允许覆盖已发布的版本。如果需要更新,必须升级版本号(如 0.1.0 -> 0.1.1)。

发布后验证

  1. 登录 central.sonatype.com,在 Activity 面板查看发布状态,通常会经历 PUBLISHING -> Published
  1. 访问 search.maven.org,搜索你的 GroupId 或 ArtifactId,通常几分钟内即可查到。
  2. 在新项目中,直接添加依赖并刷新,测试是否能正常拉取。

五、经验总结与最佳实践

  1. 设计即文档@Lock 注解的属性名(如 waitTime, spinWaitTimeParam)应尽可能直观,成为使用文档的一部分。
  2. 默认值的安全倾向:为锁设置一个合理的默认过期时间(如 expireTime),防止业务异常导致锁永不释放。
  3. 日志是关键:在 DEBUG 级别详细输出锁的获取、重试、释放日志和最终拼接的 Key,这在复杂并发场景的调试中至关重要。
  4. 测试务必覆盖双栈:编写集成测试时,需在 Spring Boot 2.x 和 3.x 两个环境下分别运行,确保兼容性。
  5. 发布流程 SOP 化:将发布步骤(尤其是 GPG 和 settings.xml 配置)写成详细的脚本或文档,能极大降低下次发布的心智负担和出错率。

六、源码与参考

  • GitHub 项目地址:https://github.com/helloworldtang/spring-annotation-distributed-lock
  • Maven Central 坐标
    • lock-key-param:0.1.0 https://search.maven.org/artifact/io.github.helloworldtang/lock-key-param/0.1.0/jar
    • distributed-lock-redis-spring:0.1.0 https://search.maven.org/artifact/io.github.helloworldtang/distributed-lock-redis-spring/0.1.0/jar

希望通过本文,你不仅获得了一个开箱即用的分布式锁工具,更能理解其背后的设计思路,并掌握将一个优秀组件交付给整个 Java 社区的标准方法。在分布式系统的世界里,好的工具和清晰的模式,能让复杂问题变得简单。

如果你在实现或发布过程中有任何问题,欢迎PR或留言讨论。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么需要这个组件?
  • 二、快速开始:5分钟上手
    • 第1步:添加依赖
  • 三、核心设计揭秘
    • 1. 整体架构流程图
    • 2. 锁的获取与释放时序图
    • 3. 关键设计决策
  • 四、发布到 Maven Central 全攻略
    • 发布前的准备
    • 发布执行命令
    • 高频“踩坑”点及解决方案
    • 发布后验证
  • 五、经验总结与最佳实践
  • 六、源码与参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档