防重复提交

使用场景

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;
}

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

相关推荐
掘金-我是哪吒4 分钟前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
许野平44 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
58沈剑2 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
ketil272 小时前
Ubuntu 安装 redis
redis
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。3 小时前
Spring Boot 配置文件
java·spring boot·后端
王佑辉3 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go