分布式锁实现原理分析
核心依赖:底层依赖 Redisson 客户端实现。
实现模式:分为 切面模式(注解式) 和 方法级别(编程式) 两种方式。
1. 分布式锁的切面实现过程
切面实现通过 AOP 拦截注解,将加锁逻辑与业务逻辑解耦,开发者只需使用 @ServiceLock 注解即可完成加锁。
1.1 核心组件
| 组件名称 | 描述 |
|---|---|
@ServiceLock | 注解定义。用于定义锁的元数据,包括:锁类型、锁名称、Key、超时时间、失败策略等。 |
ServiceLockAspect | 切面逻辑。负责拦截注解,编排“加锁 -> 执行业务 -> 解锁”的整个流程。 |
ServiceLockFactory | 锁工厂。根据注解中定义的锁类型创建具体的锁对象。 |
ServiceLocker | 锁实现接口。封装了 Redisson 的底层操作,屏蔽具体实现细节。 |
1.2 执行流程
- 拦截注解
ServiceLockAspect使用@Around("@annotation(servicelock)")切点拦截所有标记了@ServiceLock的方法。
- 解析锁信息
- 切面通过
LockInfoHandleFactory获取锁信息处理器。 - 结合注解中的
name和keys(支持 SpEL 表达式)解析并生成全局唯一的 锁名称 (lockName)。
- 切面通过
- 获取锁实例
- 调用
ServiceLockFactory.getLock(lockType)。 - 根据注解配置的
LockType(如Reentrant,Fair,Read,Write),从ManageLocker缓存中获取对应的ServiceLocker实现类(例如RedissonReentrantLocker)。
- 调用
- 尝试加锁
- 调用
ServiceLocker.tryLock(lockName, timeUnit, waitTime)。 - 底层直接调用 Redisson 的
tryLock方法,在指定的等待时间内尝试获取锁。
- 调用
- 执行业务与释放
- 成功获锁:
- 执行目标方法
joinPoint.proceed()。 - 无论业务执行是否抛出异常,均在
finally块中调用lock.unlock(lockName)确保锁被释放。
- 执行目标方法
- 获锁失败(超时):执行配置的超时策略。
- 默认策略:由
LockTimeOutStrategy定义(通常是抛出异常或记录日志)。 - 自定义策略:通过反射机制调用注解中
customLockTimeoutStrategy属性指定的方法。
- 默认策略:由
- 成功获锁:
2. 分布式锁的方法级别实现过程
方法级别实现通过工具类 ServiceLockTool 封装了标准的模板代码,适用于需要更细粒度控制锁范围的场景。
2.1 核心组件
| 组件名称 | 描述 |
|---|---|
ServiceLockTool | 工具类。对外提供 execute(无返回值)和 submit(有返回值)两种通用方法。 |
TaskRun / TaskCall | 任务接口。函数式接口,用于封装具体的业务逻辑代码块(Lambda 表达式)。 |
2.2 执行流程
- 调用工具方法
- 业务代码显式调用
serviceLockTool.execute(...)或submit(...),传入以下参数:- Task: 包含业务逻辑的 Lambda 表达式。
- Identity:
name和keys,用于生成锁名称。 - Config (可选):
lockType(锁类型)、waitTime(等待时间)。
- 业务代码显式调用
- 生成锁名称
- 工具类内部复用
LockInfoHandle逻辑,根据传入的name和keys生成标准化的锁名称。
- 工具类内部复用
- 获取锁实例
- 通过
ServiceLockFactory获取对应的ServiceLocker实例。
- 通过
- 加锁与执行
- 调用
lock.tryLock(...)尝试加锁。 - 成功:执行传入的 Lambda 表达式(
taskRun.run()或taskCall.call())。 - 释放:在
finally块中强制调用lock.unlock(lockName)。
- 调用
- 失败处理
- 如果加锁失败,默认调用
LockTimeOutStrategy.FAIL.handler(lockName)(通常抛出异常),中断业务流程。
- 如果加锁失败,默认调用
3. 总结对比
| 特性 | 切面实现 (注解式) | 方法实现 (编程式) |
|---|---|---|
| 入口 | @ServiceLock | ServiceLockTool |
| 粒度 | 粗粒度(方法级) | 细粒度(代码块级) |
| 代码侵入性 | 低 | 中 |
| 配置复杂度 | 简单 | 灵活 |
| 适用场景 | 适用于整个方法都需要同步的常规业务场景。 | 适用于需要精确控制锁持有时间、减少锁竞争的高并发场景。 |
1. 概述
在分布式系统中,为了保证数据的一致性和防止并发冲突,分布式锁是必不可少的基础组件。本项目 damai-service-lock-framework 模块基于 Redisson 客户端,封装了一套通用的分布式锁解决方案。
为了满足不同业务场景的需求,框架提供了两种加锁模式:
- 切面模式(注解式):低侵入性,适用于标准业务方法。
- 方法级别(编程式):高灵活性,适用于细粒度锁控制。
2. 模式一:切面模式(注解式实现)
切面实现的核心思想是利用 AOP(面向切面编程)拦截特定注解,将“加锁”与“释放锁”的非业务逻辑从业务代码中剥离,实现高度解耦。
2.1 核心组件
@ServiceLock(注解)- 作用:定义锁的元数据。
- 属性:包含锁类型(Reentrant, Fair, Read, Write)、锁名称前缀、Key(支持 SpEL 表达式动态解析)、超时时间、等待时间等。
ServiceLockAspect(切面)- 作用:整个锁逻辑的控制器。通过
@Around拦截标记了注解的方法,编排“获取锁 -> 执行业务 -> 释放锁”的流程。
- 作用:整个锁逻辑的控制器。通过
ServiceLockFactory(锁工厂)- 作用:工厂模式的应用。根据注解中配置的
LockType,创建并返回具体的锁对象。
- 作用:工厂模式的应用。根据注解中配置的
ServiceLocker(锁实现)- 作用:适配器模式的应用。封装 Redisson 的底层 API,屏蔽底层差异。
2.2 执行流程详解
拦截与解析
ServiceLockAspect拦截所有标记@ServiceLock的方法。- 通过
LockInfoHandleFactory解析注解参数,结合 SpEL 表达式 动态生成业务相关的唯一锁名称(Lock Name)。
获取锁实例
- 调用
ServiceLockFactory.getLock(lockType)。 - 从
ManageLocker缓存中获取对应的ServiceLocker实现类(如RedissonReentrantLocker)。
- 调用
尝试加锁 (Try Lock)
- 调用
lock.tryLock(lockName, timeUnit, waitTime)。 - 底层调用 Redisson 的
tryLock,在指定的waitTime内自旋尝试获取锁。
- 调用
业务执行与异常处理
- 成功获锁:执行
joinPoint.proceed()进入业务逻辑。无论业务是否抛出异常,均在finally块中执行lock.unlock(lockName)确保锁释放。 - 获锁失败:若等待超时,触发 LockTimeOutStrategy。
- 默认策略:抛出异常或打印日志。
- 自定义策略:反射调用注解中
customLockTimeoutStrategy指定的回调方法,实现降级逻辑。
- 成功获锁:执行
3. 模式二:方法级别(编程式实现)
对于需要更精细控制锁范围(例如只锁方法中的某几行代码),或者需要在一个方法中获取多把锁的场景,注解模式显得过于僵硬。此时,可以使用 ServiceLockTool 工具类。
3.1 核心组件
ServiceLockTool(工具类)- 作用:封装了“获取锁 -> 执行任务 -> 释放锁”的标准模板代码(Template Method Pattern)。
- API:提供
execute(无返回值)和submit(有返回值)方法。
TaskRun/TaskCall(函数式接口)- 作用:利用 Lambda 表达式封装具体的业务逻辑代码块,作为回调参数传递给工具类。
3.2 执行流程详解
调用工具方法
- 业务代码显式调用
serviceLockTool.execute(...)。 - 入参:业务逻辑 Lambda (
TaskRun)、锁名称name、动态 Keykeys、锁类型lockType等。
- 业务代码显式调用
标准化锁名称
- 工具类内部复用
LockInfoHandle逻辑,确保与注解模式生成锁名称的规则一致。
- 工具类内部复用
模板执行
- 加锁:通过
ServiceLockFactory获取锁并尝试tryLock。 - 回调:加锁成功后,回调传入的
taskRun.run()或taskCall.call()执行业务。 - 释放:在
finally块中强制unlock。
- 加锁:通过
失败兜底
- 若加锁失败,直接调用
LockTimeOutStrategy.FAIL处理(通常抛出业务异常中断流程)。
- 若加锁失败,直接调用
代码示例:
// 编程式加锁示例
serviceLockTool.execute(() -> {
// 这里是需要加锁的核心业务代码
// ...
}, "order_create", new String[]{orderId});4. 总结与对比
项目中同时保留这两种实现方式,旨在平衡开发的便捷性与灵活性。
| 特性 | 切面模式 (Annotation) | 方法级别 (Programmatic) |
|---|---|---|
| 入口 | @ServiceLock 注解 | ServiceLockTool 工具类 |
| 粒度 | 方法级(整个方法体被锁包裹) | 代码块级(可控制只锁某几行) |
| 侵入性 | 低(业务代码无感知) | 中(需要显式调用工具类) |
| 适用场景 | 90% 的常规业务,如防止重复提交 | 需要减少锁持有时间、复杂锁逻辑的场景 |
| 锁释放 | AOP 自动处理 | 工具类模板自动处理 |
| 优点 | 代码整洁,配置简单 | 灵活性高,性能优化空间大 |
最佳实践建议: 优先使用 @ServiceLock 注解开发,保持代码简洁;当发现锁竞争激烈,需要缩小锁范围以提升吞吐量时,再重构为 ServiceLockTool 编程式锁。
这是一个非常高质量的简历项目经验挖掘和面试对答梳理。内容涵盖了架构设计(AOP vs 编程式)、并发痛点(事务与锁的时序)、底层细节(SpEL 解析)以及兜底策略(降级)。
以下是为你整理好的 Markdown 格式技术博客,既可以作为你的面试复习笔记,也可以直接作为技术文章发布。
这是一个非常高质量的简历项目经验挖掘和面试对答梳理。内容涵盖了架构设计(AOP vs 编程式)、并发痛点(事务与锁的时序)、底层细节(SpEL 解析)以及兜底策略(降级)。
以下是为你整理好的 Markdown 格式技术博客,既可以作为你的面试复习笔记,也可以直接作为技术文章发布。
public String localLockCreateOrder(String lockKeyPrefix,ProgramOrderCreateDto programOrderCreateDto,LockTask<String> lockTask){
List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
List<Long> ticketCategoryIdList = new ArrayList<>();
if (CollectionUtil.isNotEmpty(seatDtoList)) {
//按照票档id进行排序,这样为了避免不同请求获取票档的顺序不同加锁而可能产生的死锁问题
ticketCategoryIdList =
seatDtoList.stream().map(SeatDto::getTicketCategoryId).distinct().sorted().collect(Collectors.toList());
}else {
ticketCategoryIdList.add(programOrderCreateDto.getTicketCategoryId());
}
//本地锁集合
List<ReentrantLock> localLockList = new ArrayList<>(ticketCategoryIdList.size());
//加锁成功的本地锁集
List<ReentrantLock> localLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
//根据统计出的票档id获得本地锁集合
for (Long ticketCategoryId : ticketCategoryIdList) {
//锁的key为d_program_order_create_v3_lock-programId-ticketCategoryId
String lockKey = StrUtil.join("-",lockKeyPrefix,
programOrderCreateDto.getProgramId(),ticketCategoryId);
//获得本地锁实例
ReentrantLock localLock = localLockCache.getLock(lockKey,false);
//添加到本地锁集合
localLockList.add(localLock);
}
//循环本地锁进行加锁
for (ReentrantLock reentrantLock : localLockList) {
try {
reentrantLock.lock();
}catch (Throwable t) {
//如果加锁出现异常,则终止
break;
}
localLockSuccessList.add(reentrantLock);
}
try {
//执行真正的逻辑
return lockTask.execute();
}finally {
//再循环解锁本地锁
for (int i = localLockSuccessList.size() - 1; i >= 0; i--) {
ReentrantLock reentrantLock = localLockSuccessList.get(i);
try {
reentrantLock.unlock();
}catch (Throwable t) {
log.error("local lock unlock error",t);
}
}
}
}