防重复提交

使用场景

1. 针对前端页面

网络问题导致用户重复点击,发了多次请求引起的重复提交

2. 针对系统间接口调用

因网络超时重试或者程序bug导致的重复请求

3. 通用的业务层面的验证

比如要限制某个接口的一些参数的组合在规定的时间内只允许请求一次

实现方式

通过注解的方式,获取能标识重复请求的唯一的 key 作为 redis key,允许指定超时时间,在这段时间内,这种请求只允许通过一次。

获取唯一标识有三种方式,对应三种处理策略,只允许指定一个。三种方式分别为:

1、调用方自己传过来一个唯一的 id,放到请求的 header 中

2、能获取到登录用户的登录信息的情况下,通过 userId 和请求的 Uri 的组合生成唯一的 key

3、通过指定请求的参数的字段组合,通过这些字段组合生成唯一 key,请求字段通过 spEl 来指定。

使用场景示例

1. 前端表单提交的场景

解决方案:

1)使用策略1,基于唯一id的验证。

在打开一个提交页面的时候,前端可以生成一个全局唯一的 token 值,可自己生成或者调后端接口,请求后端提交接口的时候带上这个 token 值。

这样如果在第一次请求没有相应,用户快速点击的时候,因为在同一个页面,还没有刷新,所以该页面的所有请求携带的 token 值是相同的,这样只有第一次请求被正确处理。

2)使用策略2,基于用户的验证。这种对应前端不需要改动,后端自动获取用户信息和请求的URI。

2. 接口调用超时的场景

如果请求中参数有一个字段是唯一的,则可以使用策略1和3,如果是组合唯一则使用策略3

3. 通用的业务层面的接口参数的验证

使用策略3,通过spEl在注解中指定要验证的组合字段,举例:

ini 复制代码
@RepeatCheck(expireTime = 2000,
        keyExpression = "#index + '_' + #value + '_'+ #fundDTO.fundType + '_' + #fundDTO.fundChannel")

实战

接下来我们重点讲解业务层通用去重校验,实现思路:

  1. 注解式:利用 AOP 切面能力
  2. 业务层自定义唯一 Key:利用 spEL 表达式解析
  3. redis 存储校验

大致实现如下:

java 复制代码
    @Before("@annotation(annotation)")
    public Object repeatCheck(JoinPoint joinPoint, ResubmitCheck annotation) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String[] conditionExpressions = annotation.conditionExpressions();
        if (ExpressionUtils.getConditionValue(args, conditionExpressions) || ArrayUtils.isEmpty(args)) {
            this.doCheck(annotation, args);
        }

        return true;
    }


    private void doCheck(@NonNull ResubmitCheck annotation, Object[] args) {
        if (annotation == null) {
            throw new NullPointerException("annotation cannot be null");
        }

        String keyExpressions = annotation.keyExpression();
        String message = annotation.message();
        boolean withUserInfoInKey = annotation.withUserInfoInKey();
        String methodDesc = this.request.getMethod();
        String uri = this.request.getRequestURI();

        StringBuilder bizKey = new StringBuilder(64);
        Object[] argsKeys = ExpressionUtils.getExpressionValue(args, keyExpressions);
        int length = argsKeys.length;

        for(int i = 0; i < length; ++i) {
            Object obj = argsKeys[i];
            bizKey.append(obj.toString());
        }

        StringBuilder keyBuilder = new StringBuilder();
        String userId = Objects.isNull(this.request.getHeader("uerId")) ? "0" : this.request.getHeader("userId");
        keyBuilder.append("repeatCheck::").append(withUserInfoInKey ? userId + "::" : "").append(uri).append("::").append(methodDesc).append("::").append(bizKey.toString());
        if (this.stringRedisTemplate.opsForValue().get(keyBuilder.toString()) != null) {
            throw new ResubmitException(10001, StringUtils.isBlank(message) ? "重复提交" : message);
        } else {
            this.stringRedisTemplate.opsForValue().set(keyBuilder.toString(), "", annotation.expireTime(), annotation.timeUnit());
        }
    }

注解定义:

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RepeatCheck {
    String keyExpression();

    String message() default "重复提交";

    boolean withUserInfoInKey() default false;

    long expireTime() default 1L;

    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

如此,一个通用的防重组件大致完成了...

相关推荐
IT_陈寒2 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
Thneonl2 小时前
Celery 生产踩坑:1000 任务积压与 acks_late 双重执行
后端·python
卷福同学2 小时前
第一次当面试官有感
后端·面试
苏三说技术2 小时前
为什么越来越多人用 OnlyOffice?
后端
知守观2 小时前
@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
后端·spring
羑悻2 小时前
Codex + Seed-2.1-pro 实测:多模态理解 + Coding Agent 能扛住真实仓库吗?
后端
CopyCode2 小时前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
美好世界2 小时前
Codex 源码导读:第五部分——事件出口与多入口适配
架构
颜进强2 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
小小张说故事2 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python