基于 AOP 与 Redisson 的分布式锁实现:自动加锁、解锁与 SpEL 参数解析
一、前言
在分布式系统中,为了保证多个服务实例间的数据一致性,分布式锁是常见的技术手段。本文将介绍如何使用 Spring AOP、Redisson 和 SpEL 表达式,实现一套声明式的分布式锁方案:通过注解标注方法,自动完成加锁、解锁,并支持从方法参数中灵活解析锁的 Key。
核心特性:
- 注解驱动,无需在业务代码中手写加锁逻辑
- 支持 Redisson 分布式锁,基于 Redis 实现
- 支持 SpEL 表达式解析,从方法参数动态构建锁 Key
- 支持“立即失败”与“等待重试”两种加锁策略
二、注解设计
2.1 @Key 注解:参数级 SpEL 解析
@Documented
@Retention(RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface Key {
String expr() default "";
}
作用: 标注在方法参数上,表示该参数参与锁 Key 的生成。通过 expr 可指定 SpEL 表达式,从参数对象中提取字段值;若 expr 为空,则直接使用参数的 toString()。
2.2 @Lock 注解:方法级加锁配置
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface Lock {
String prefix(); // 锁 Key 前缀,用于业务区分
boolean isWait() default false; // 是否等待获取锁
long waitTime() default 3L; // 等待时长(秒),仅 isWait=true 时有效
ErrorCodeEnum failCode() default ErrorCodeEnum.OK; // 获取锁失败时抛出的错误码
}
参数说明:
| 参数 | 说明 |
|---|---|
prefix | 锁 Key 的固定前缀,便于按业务分类和排查 |
isWait | false:获取不到锁立即失败;true:等待重试 |
waitTime | 等待秒数,仅在 isWait=true 时生效 |
failCode | 加锁失败时抛出的业务错误码 |
三、切面实现:LockAspect
package com.trendinguse.cwcopen.novel.core.aspect;
import com.trendinguse.cwcopen.novel.core.annotation.Key;
import com.trendinguse.cwcopen.novel.core.annotation.Lock;
import com.trendinguse.cwcopen.novel.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@RequiredArgsConstructor
public class LockAspect {
private final RedissonClient redissonClient;
private static final String KEY_PREFIX = "Lock";
private static final String KEY_SEPARATOR = "::";
@Around(value = "@annotation(com.trendinguse.cwcopen.novel.core.annotation.Lock)")
@SneakyThrows
public Object doAround(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
Lock lock = targetMethod.getAnnotation(Lock.class);
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
joinPoint.getArgs());
RLock rLock = redissonClient.getLock(lockKey);
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
try {
return joinPoint.proceed();
} finally {
rLock.unlock();
}
}
throw new BusinessException(lock.failCode());
}
private String buildLockKey(String prefix, Method method, Object[] args) {
StringBuilder builder = new StringBuilder();
if (StringUtils.hasText(prefix)) {
builder.append(KEY_SEPARATOR).append(prefix);
}
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(Key.class)) {
builder.append(KEY_SEPARATOR);
Key key = parameters[i].getAnnotation(Key.class);
builder.append(parseKeyExpr(key.expr(), args[i]));
}
}
return builder.toString();
}
private String parseKeyExpr(String expr, Object arg) {
if (!StringUtils.hasText(expr)) {
return arg.toString();
}
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
return expression.getValue(arg, String.class);
}
}
3.1 执行流程
- 拦截:
@Around拦截所有带@Lock的方法 - 构建锁 Key:
buildLockKey根据prefix及带@Key的参数生成唯一 Key - 加锁:根据
isWait选择tryLock()或tryLock(waitTime, SECONDS) - 执行业务:加锁成功后执行
proceed() - 解锁:在
finally中调用unlock(),确保异常时也会释放锁 - 失败处理:加锁失败时抛出
BusinessException,使用failCode
3.2 锁 Key 规则
最终格式:Lock::prefix::param1::param2::...。例如:
prefix = "order"- 参数 1:
@Key Long orderId→123 - 参数 2:
@Key(expr = "#{userId}") User user→10086
则锁 Key 为:Lock::order::123::10086。
3.3 SpEL 解析说明
使用 TemplateParserContext 时,表达式需使用 #{...} 形式,例如:
@Key(expr = "#{id}"):从参数对象中取id字段@Key(expr = "#{userId}"):取userId字段expr为空:直接使用arg.toString()
参数遍历与锁 Key 构建
buildLockKey 负责遍历方法参数,将带 @Key 的参数值拼接成锁 Key 的一部分。核心代码如下:
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(Key.class)) {
builder.append(KEY_SEPARATOR);
Key key = parameters[i].getAnnotation(Key.class);
builder.append(parseKeyExpr(key.expr(), args[i]));
}
}
return builder.toString();
流程解析:
-
获取参数元数据:
method.getParameters()返回Parameter[],与args[]下标一一对应,即parameters[i]对应args[i]。 -
注解判断:
parameters[i].isAnnotationPresent(Key.class)判断第i个参数是否标注了@Key,只有带@Key的参数才参与锁 Key 的生成。 -
逐参数处理:遍历每个参数,先追加
KEY_SEPARATOR(::),保证各段之间用分隔符连接。 -
读取表达式:
parameters[i].getAnnotation(Key.class)获取@Key实例,通过key.expr()得到 SpEL 表达式(可能为空)。 -
解析并追加:
parseKeyExpr(key.expr(), args[i])以当前参数值args[i]为根对象,根据expr求值得到字符串,并追加到builder。 -
返回值:最终
builder形如::prefix::val1::val2,与KEY_PREFIX拼接后得到完整锁 Key。
为何必须用 parameters 而非 args 判断注解? 注解是编译期元数据,依附在方法参数的声明上,保存在字节码中;args[] 是运行时的实际参数值(普通 Java 对象)。运行时对象本身没有“参数声明”信息,无法通过 args[i] 得知它对应的是哪个参数、是否被标注了 @Key。只有通过反射获取的 Parameter 对象(method.getParameters())才带有参数声明的元数据,因此必须用 parameters[i].isAnnotationPresent(Key.class) 来判断该参数是否使用了 @Key,而用 args[i] 仅作为求值时的根对象传给 parseKeyExpr。
SpEL 解析流程
parseKeyExpr 中 SpEL 的解析逻辑如下:
private String parseKeyExpr(String expr, Object arg) {
if (!StringUtils.hasText(expr)) {
return arg.toString();
}
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
return expression.getValue(arg, String.class);
}
-
空表达式:若
expr为空,直接返回arg.toString(),适用于简单类型(如Long、String)。 -
TemplateParserContext 作用:
TemplateParserContext默认以#{为前缀、}为后缀,会从expr中提取#{与}之间的内容作为真正的 SpEL 表达式。例如expr = "#{userId}"时,实际参与解析的是userId,而不是整个字符串。 -
解析阶段:
parseExpression(expr, new TemplateParserContext())会:- 识别
#{和}的边界; - 将中间部分
userId解析为 SpEL 表达式; - 返回可复用的
Expression对象。
- 识别
-
求值阶段:
expression.getValue(arg, String.class)以arg为根对象(root object) 执行表达式:#{userId}→ 等价于arg.getUserId()或arg["userId"];#{id}→ 等价于arg.getId();- 支持链式访问,如
#{order.userId}表示arg.getOrder().getUserId()。
-
类型转换:
getValue(arg, String.class)的第二个参数表示期望的返回类型,SpEL 会将求值结果转为String,便于拼接进锁 Key。
四、使用示例
4.1 基于简单参数
@Lock(prefix = "book", failCode = ErrorCodeEnum.BOOK_ALREADY_BORROWED)
public void borrowBook(@Key Long bookId, @Key Long userId) {
// 业务逻辑:借书
}
锁 Key:Lock::book::123::10086
4.2 基于对象与 SpEL
public class OrderReq {
private Long orderId;
private Long userId;
// getters / setters
}
@Lock(prefix = "order", isWait = true, waitTime = 5)
public void createOrder(@Key(expr = "#{orderId}") OrderReq req) {
// 业务逻辑:创建订单
}
锁 Key:Lock::order::10001(从 req.getOrderId() 解析)
4.3 立即失败
@Lock(prefix = "inventory", failCode = ErrorCodeEnum.SYSTEM_BUSY)
public void deductStock(@Key Long productId) {
// 获取不到锁立即抛出 BusinessException
}
五、依赖配置
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<!-- Spring AOP(通常已有) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
六、注意事项
- 锁粒度:尽量只对必要的业务范围加锁,避免锁 Key 过宽导致并发下降
- 异常与解锁:
finally中unlock()会保证方法结束时释放锁,异常也不会导致死锁 - 空值参数:带
@Key的参数为null时,arg.toString()会抛 NPE,建议对关键参数做非空校验 - SpEL 语法:使用
TemplateParserContext时,expr必须为#{...}形式