rouyi后端开发文档-幂等性(防止重复提交)

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": "重复请求,请稍后重试"
}

实现代码:

image.png

对应的 AOP 切面是 IdempotentAspect (opens new window) :

IdempotentAspect 切面

对应的 Redis Key 的前缀是 idempotent:%s,可见 IdempotentRedisDAO (opens new window) 类,如下图所示:

IdempotentRedisDAO 存储

redis存储例子:

image.png

扩展

解析器接口:

/**
 * 幂等 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);
        }
    }

}

这两个解析器有什么区别呢?

  1. DefaultIdempotentKeyResolver :
    • 默认实现 :DefaultIdempotentKeyResolver 是默认的键解析器,它是最简单的实现方式。
    • 解析方式 :它使用方法名和方法参数组合成字符串,然后通过 MD5 进行哈希处理,生成唯一的关键信息。
    • 固定逻辑 :它的解析逻辑是固定的,无法根据具体的业务需求进行自定义。它适合那些不需要复杂关键信息逻辑的幂等操作。
  2. 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 方法,只有第一次调用会生效,后续的调用会被幂等性控制机制拦截。

rouyi后端开发文档地址:幂等性(防重复提交) | ruoyi-vue-pro 开发指南

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值