1.什么是重复提交
例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。
实现原理
它的实现原理非常简单,针对相同参数的方法,一段时间内,有且仅能执行一次。执行流程如下:
① 在方法执行前,根据参数对应的 Key 查询是否存在。
- 如果 存在 ,说明正在执行中,则进行报错。
- 如果 不在 ,则计算参数对应的 Key,存储到 Redis 中,并设置过期时间,即标记正在执行中。
默认参数的 Redis Key 的计算规则由 DefaultIdempotentKeyResolver (opens new window) 实现,使用 MD5(方法名 + 方法参数),避免 Redis Key 过长。
② 方法执行完成,不会主动删除参数对应的 Key。
从本质上来说,idempotent
包提供的幂等特性,本质上也是基于 Redis 实现的分布式锁。
③ 如果方法执行时间较长,超过 Key 的过期时间,则 Redis 会自动删除对应的 Key。因此,需要大概评估下,避免方法的执行时间超过过期时间。
@Idempotent
注解
声明在方法上,表示该方法需要开启幂等性。代码如下:
// UserController.java
@Idempotent(timeout = 10, timeUnit = TimeUnit.SECONDS, message = "正在添加用户中,请勿重复提交")
@PostMapping("/user/create")
public String createUser(User user){
userService.createUser(user);
return "添加成功";
}
再次调用接口,被幂等性拦截,执行失败。
{
"code": 900,
"data": null,
"msg": "重复请求,请稍后重试"
}
实现代码:
对应的 AOP 切面是 IdempotentAspect (opens new window) :
对应的 Redis Key 的前缀是 idempotent:%s
,可见 IdempotentRedisDAO (opens new window) 类,如下图所示:
redis存储例子:
扩展
解析器接口:
/**
* 幂等 Key 解析器接口
*
* @author 芋道源码
*/
public interface IdempotentKeyResolver {
/**
* 解析一个 Key
*
* @param idempotent 幂等注解
* @param joinPoint AOP 切面
* @return Key
*/
String resolver(JoinPoint joinPoint, Idempotent idempotent);
}
默认解析器:
/**
* 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
*
* 为了避免 Key 过长,使用 MD5 进行“压缩”
*
* @author 芋道源码
*/
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
return SecureUtil.md5(methodName + argsStr);
}
}
基于Spring EL表达式的解析器:
/**
* 基于 Spring EL 表达式,
*
* @author 芋道源码
*/
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
// 获得被拦截方法参数名列表
Method method = getMethod(joinPoint);
Object[] args = joinPoint.getArgs();
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
// 准备 Spring EL 表达式解析的上下文
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
if (ArrayUtil.isNotEmpty(parameterNames)) {
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
}
// 解析参数
Expression expression = expressionParser.parseExpression(idempotent.keyArg());
return expression.getValue(evaluationContext, String.class);
}
private static Method getMethod(JoinPoint point) {
// 处理,声明在类上的情况
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (!method.getDeclaringClass().isInterface()) {
return method;
}
// 处理,声明在接口上的情况
try {
return point.getTarget().getClass().getDeclaredMethod(
point.getSignature().getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
这两个解析器有什么区别呢?
- DefaultIdempotentKeyResolver :
- 默认实现 :
DefaultIdempotentKeyResolver
是默认的键解析器,它是最简单的实现方式。 - 解析方式 :它使用方法名和方法参数组合成字符串,然后通过 MD5 进行哈希处理,生成唯一的关键信息。
- 固定逻辑 :它的解析逻辑是固定的,无法根据具体的业务需求进行自定义。它适合那些不需要复杂关键信息逻辑的幂等操作。
- 默认实现 :
- ExpressionIdempotentKeyResolver :
- 自定义实现 :
ExpressionIdempotentKeyResolver
允许开发者使用 Spring EL 表达式来自定义关键信息的生成方式。 - 解析方式 :它根据
idempotent.keyArg()
表达式来动态生成关键信息。开发者可以在表达式中使用方法参数、返回值或其他上下文信息。 - 灵活性 :它非常灵活,允许根据具体需求定义复杂的关键信息生成逻辑。这对于需要根据不同的方法或业务场景生成不同关键信息的情况非常有用。
- 自定义实现 :
总之,DefaultIdempotentKeyResolver
是一个简单而固定的实现,适合不需要复杂关键信息生成逻辑的情况。而 ExpressionIdempotentKeyResolver
则更为灵活,允许根据具体需求自定义关键信息的生成方式,这对于需要动态生成关键信息的幂等操作非常有用。开发人员可以根据具体情况选择合适的解析器。
使用 ExpressionIdempotentKeyResolver 的例子
下面是一个使用 ExpressionIdempotentKeyResolver
的简单示例:
首先,假设有一个服务类,其中包含一个幂等性操作:
@Service
public class OrderService {
@Idempotent(keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#userId + '-' + #productId")
public void createOrder(int userId, int productId) {
// 创建订单的业务逻辑
}
}
在上述示例中,createOrder
方法被标记为 @Idempotent
,并且使用 ExpressionIdempotentKeyResolver
作为键解析器,同时指定了一个 Spring EL 表达式 #userId + '-' + #productId
作为 keyArg
。
这里,我们使用 Spring EL 表达式来动态生成关键信息,以确保对于每个不同的 userId
和 productId
组合,都会生成不同的幂等性关键。这样,即使相同的用户和产品尝试多次调用 createOrder
方法,只有第一次调用会生效,后续的调用会被幂等性控制机制拦截。