分布式锁基础组件
分布式锁基础组件
1. 概述
本组件提供了一套企业级的分布式锁解决方案,旨在以简便、高效、可靠的方式解决分布式环境下的并发控制问题。它基于 Spring AOP 和 Redisson 构建,通过声明式注解和编程式模板,极大地简化了分布式锁的开发工作。
核心特性
- 易于扩展的设计: 基于策略模式 (Strategy) 和工厂模式 (Factory) 构建,您可以轻松添加新的锁实现。
- 丰富的锁类型: 原生支持可重入锁 (Reentrant Lock)、公平锁 (Fair Lock)、读锁 (Read Lock) 和写锁 (Write Lock),满足不同业务场景的需求。
- 两种使用方式:
@DLock
注解: 通过 AOP 实现声明式加锁,对业务代码零侵入,是推荐的使用方式。LockTemplate
编程式 API: 提供灵活的编程式调用,适用于更复杂的业务逻辑。
- 高级功能:
- 事务兼容: 通过 AOP 的
@Order
精确控制执行顺序,确保在 Spring 事务 (@Transactional
) 外部获取和释放锁,避免因锁失败导致不必要的事务回滚。 - 本地锁优先: 在尝试获取分布式锁之前,首先获取 JVM 本地锁。这一优化能大幅提升高并发场景下的性能,有效降低对 Redis 的直接冲击。
- 动态 Key 生成: 注解的
keys
属性支持 SpEL (Spring Expression Language) 表达式,可以根据方法参数动态生成锁的 Key,实现对具体资源的精细化锁定。
- 事务兼容: 通过 AOP 的
- 底层依赖: 深度集成并推荐使用
Redisson
作为其分布式锁的实现客户端。
2. 快速入门
2.1. 添加 Maven 依赖
将以下依赖项添加到您的 pom.xml
文件中:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
2.2. 配置 Redis 连接
在您的 application.yml
(或 application.properties
) 文件中配置 Redisson 的 Redis 连接信息。
单机模式示例:
spring:
redis:
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: "your-redis-password" # 如果没有密码,可以移除此行
database: 0
集群模式示例:
spring:
redis:
redisson:
config: |
clusterServersConfig:
nodeAddresses:
- "redis://192.168.1.1:7001"
- "redis://192.168.1.2:7002"
- "redis://192.168.1.3:7003"
password: "your-redis-password"
2.3. 启用组件
确保您的 Spring Boot 主程序能够扫描到本组件的包路径(例如 com.yourcompany.lock
)。
@SpringBootApplication(scanBasePackages = {"com.yourcompany.service", "com.yourcompany.lock"})
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
3. 使用指南
本组件提供了两种核心的加锁方式:注解式和编程式。
3.1. 方式一:使用 @DLock
注解 (推荐)
这是最简单、最常用的方式。只需在需要加锁的方法上添加 @DLock
注解即可。
示例:防止订单重复取消
package com.yourcompany.service;
import com.yourcompany.lock.annotation.DLock;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
/**
* @param orderId 订单ID
* @param userId 用户ID
*/
@DLock(name = "order:cancel", keys = {"#orderId"}, waitTime = 5000)
@Transactional // @DLock 的 @Order(1) 保证它在 @Transactional 之前执行
public void cancelOrder(String orderId, Long userId) {
// ...
// 这里的代码将在获取到 order:cancel:xxx 的锁之后执行
// 模拟业务耗时
// ...
System.out.println("取消订单业务处理完成, orderId: " + orderId);
}
}
在上面的例子中:
- 最终的锁 Key 将会是
order:cancel:
加上orderId
参数的值,例如order:cancel:ORD123456
。 keys = {"#orderId"}
使用了 SpEL 表达式来动态获取方法参数。waitTime = 5000
表示如果锁被占用,当前线程将最多等待 5 秒。如果超时仍未获取到锁,将抛出LockAcquisitionException
。@DLock
的切面优先级高于@Transactional
,确保了先加锁、后开启事务的正确顺序。
@DLock
注解参数详解
属性 | 类型 | 描述 | 默认值 |
---|---|---|---|
name | String | 必需。锁的名称或前缀,用于构造锁 Key 的固定部分。 | - |
keys | String[] | 必需。锁 Key 的动态部分。支持 SpEL 表达式(以 # 开头)来引用方法参数。多个 key 会用 : 连接。 | - |
lockType | LockType | 锁的类型。可选值为 REENTRANT , FAIR , READ , WRITE 。 | LockType.REENTRANT |
waitTime | long | 获取锁的等待时间。-1 表示阻塞等待直到获取锁。正数表示最长等待时间。 | -1 |
leaseTime | long | 锁的持有时间(租期)。-1 表示启用 Redisson 的看门狗 (Watchdog) 机制,锁会自动续期,避免业务未执行完锁就过期。 | -1 |
unit | TimeUnit | waitTime 和 leaseTime 的时间单位。 | TimeUnit.MILLISECONDS |
failMessage | String | 获取锁失败时抛出的 LockAcquisitionException 中包含的错误信息。 | "操作频繁,请稍后重试" |
3.2. 方式二:使用 LockTemplate
编程式
对于更复杂的场景,或者当您无法使用 AOP 注解时,可以使用 LockTemplate
。
示例:使用读锁查询库存
package com.yourcompany.service;
import com.yourcompany.lock.enums.LockType;
import com.yourcompany.lock.template.LockTemplate;
import org.springframework.stereotype.Service;
@Service
public class StockService {
private final LockTemplate lockTemplate;
public StockService(LockTemplate lockTemplate) {
this.lockTemplate = lockTemplate;
}
public String checkStock(String productId) {
// 构造锁的 Key
String lockKey = "stock:check:" + productId;
// 使用 LockTemplate 执行
return lockTemplate.execute(
lockKey,
LockType.READ, // 使用读锁
() -> {
// ...
// 这部分代码在获取到读锁后执行
// 模拟数据库查询
System.out.println("获取读锁成功,开始查询库存...");
Thread.sleep(100);
System.out.println("库存查询完毕。");
return "库存充足";
// ...
}
);
}
}
lockTemplate.execute(...)
方法会自动处理锁的获取和释放。您只需关注被锁保护的业务逻辑即可,该逻辑通过 Callable
Lambda 表达式传入。如果获取锁失败,同样会抛出 LockAcquisitionException
。
4. 核心概念与设计
4.1. 性能优化:本地锁优先
在高并发场景下,大量线程同时请求 Redis 获取同一个分布式锁,会给 Redis 造成巨大压力。本组件通过内置的 Guava Cache
实现了一个**本地锁(JVM 锁)**缓存。
执行流程:
- 获取本地锁: 线程首先尝试获取与分布式锁 Key 对应的 JVM 锁 (
ReentrantLock
)。 - 获取分布式锁: 只有成功获取到本地锁的一个线程,才会继续去请求 Redis 获取分布式锁。
- 执行业务逻辑。
- 释放锁: 先释放分布式锁,再释放本地锁。
这种机制确保了在单个 JVM 实例内部,只有一个线程会去竞争分布式锁,极大地减少了对 Redis 的网络请求和竞争,从而提升了整体性能和吞吐量。
4.2. 事务集成
本组件的 AOP 切面 @DLockAspect
被标记为 @Order(1)
。在 Spring AOP 中,@Order
的值越小,优先级越高。Spring 的事务注解 @Transactional
默认优先级较低。
因此,本组件能保证:
@DLock
的 AOP advice (通知) 在@Transactional
的 advice 之前执行。
这意味着程序的执行顺序是:
DLockAspect
: 获取锁。TransactionalAspect
: 开启事务。- 执行业务方法。
TransactionalAspect
: 提交或回滚事务。DLockAspect
: 释放锁。
这样做的好处是,如果获取锁失败,程序会直接抛出异常,而不会进入并开启一个注定要失败的数据库事务,从而避免了不必要的资源开销。
5. 异常处理
当一个线程在指定的 waitTime
内未能获取到分布式锁时,组件会抛出一个非受检异常 com.yourcompany.lock.exception.LockAcquisitionException
。
建议在您的应用中设置一个全局异常处理器 (@RestControllerAdvice
) 来捕获此异常,并向客户端返回一个友好的提示信息。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(LockAcquisitionException.class)
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) // 429
public YourApiResponse handleLockAcquisitionException(LockAcquisitionException ex) {
log.warn("获取分布式锁失败: {}", ex.getMessage());
return YourApiResponse.fail(429, ex.getMessage());
}
}
6. 扩展性
得益于策略模式,您可以轻松地添加自定义的锁实现(例如,基于 Zookeeper 的锁)。
- 在
LockType
枚举中添加新类型。 - 创建新的策略类: 实现
DistributedLock
接口,并使用@Component
注解将其注册为 Spring Bean。 - 实现
tryLock
和unlock
逻辑。
LockFactory
会在启动时自动检测并注册所有 DistributedLock
接口的实现,无需修改工厂代码。