背景

进来开发了一个新增的接口,有开发伙伴反馈连续点击新增保存时,增加了两条数据

原因

正常的业务流程应该是点击 “新增保存” 按钮,等待返回成功,跳转查询列表,or 返回失败,当前页面提醒。问题原因如下:

1.后端没有做防重复提交

2.前端伙伴没有在点击新增保存按钮时loading等待返回结果。

解决方案

该问题前端做放重复提交or后端做防重复提交均可,因主要做后端,所以提供一个后端解决方案。

前端打开新增or修改页面时,生成一个唯一字符串(可调用后端接口生成or前端生成),然后请求新增or修改接口时,携带该参数,后端进行分布式锁竞争校验。

代码

No bb, show coding

代码地址:framework: 项目结构https://gitee.com/kwins/framework

注解定义(切面切入点标记,及 接口定制化参数的配置)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交
 * @author kwin
 * @Date 2022/1/16 14:59
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {

    /**
     * 锁过期时间
     * @return
     */
    int timeout() default 5;

    /**
     * 加锁等待时间
     * @return
     */
    int waittime() default 0;

    /**
     * 锁过期时间单位
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 锁的位置
     */
    String location() default "RepeatSubmit";

    /**
     * 参数位置
     */
    int argInx() default 0;

    /**
     * 参数名称
     */
    String argName() default "";
}

上述分布式锁过期时间默认是5s,具体时间应该根据接口及业务具体分析。

参数位置:前端传入的唯一字符串在接口入参的位置,从0开始

参数名称:如果上述参数位置所对应的参数为对象,该参数为对象的field name。

切面(切入点:使用了RepeatSubmit 注解的方法)

import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import com.kwin.demo.server.framework.common.lock.RedissLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.Date;

/**
 * @author kwin
 * @Date 2022/1/16 15:17
 **/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {

    private static final String REPEAT_LOCK_PREFIX = "repeatLock_";

    /**
     * 切点
     * @param repeatSubmit
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void repeatPoint(RepeatSubmit repeatSubmit) {
    }

    /**
     *
     * @return
     */
    @Around(value = "repeatPoint(repeatSubmit)")
    public Object doAround(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) {
        String key = REPEAT_LOCK_PREFIX + repeatSubmit.location();

        String argName = repeatSubmit.argName();
        int argInx = repeatSubmit.argInx();
        Object[] args = joinPoint.getArgs();

        String suffix;
        if(!StringUtils.hasText(argName)) {
            suffix = String.valueOf(args[argInx]);
        } else {
            suffix = generateSuffix(args[argInx], argName);
        }

        key = key + ":" + suffix;
        if(!RedissLockUtil.tryLock(key, repeatSubmit.timeUnit(), repeatSubmit.waittime(), repeatSubmit.timeout())) {
            return "操作过于频繁,请稍后重试";
        }

        System.out.println(new Date());
        try {
            Object proceed = joinPoint.proceed();
            return proceed;
        } catch (Throwable throwable) {
            log.error("", throwable);
            throw new RuntimeException(throwable.getMessage());
        } finally {
            try {
                RedissLockUtil.unlock(key);
            } catch (Exception ex) {
                log.error("解锁失败,key is {}", key, ex);
            }

        }
    }


    public String generateSuffix(Object obj, String argName) {
        Class clzz = obj.getClass();
        Field argField = null;
        try {
            argField = clzz.getDeclaredField(argName);
            argField.setAccessible(Boolean.TRUE);
            Object arg = argField.get(obj);
            return String.valueOf(arg);
        } catch (NoSuchFieldException e) {
            log.error("no such field", e);
        } catch (IllegalAccessException e) {
            log.error("IllegalAccessException ", e);
        }
        return "SUFFIX";
    }
}

这部分代码,首先通过注解的argInx获取到唯一字符串,or通过argInx和argName反射获取到唯一字符串。

然后对生成的key进行分布式锁竞争,竞争到锁执行业务惭怍,竞争失败则抛回提示“操作过于频繁,请稍后重试”。

关于分布式锁的实现借助redission,具体自己翻代码,或者参考自己项目实现。

验证

测试代码

import com.alibaba.fastjson.JSON;
import com.kwin.demo.api.dto.req.TestReq;
import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.*;

/**
 * @author kwin
 * @Date 2022/1/16 18:36
 **/
@RestController
public class TestController {

    @GetMapping("/test/{random}")
    @RepeatSubmit(location = "test", timeout = 10)
    public String test(@PathVariable("random") String random) throws InterruptedException {
        Thread.sleep(5000);
        return random;
    }
}

同时传入相同random,调用该接口(这点需要注意:谷歌浏览器同时只能对同一个URL提出一个请求,如果有更多的请求的话,则会串行执行。如果请求阻塞,后续相同请求也会阻塞。),如下图:

java 防止接口并发 防止接口重复调用_java

 

java 防止接口并发 防止接口重复调用_防重复提交_02

测试代码(argInx 和argName同时使用)

import com.alibaba.fastjson.JSON;
import com.kwin.demo.api.dto.req.TestReq;
import com.kwin.demo.server.framework.common.repeatsubmit.annotation.RepeatSubmit;
import org.springframework.web.bind.annotation.*;

/**
 * @author kwin
 * @Date 2022/1/16 18:36
 **/
@RestController
public class TestController {


    @PostMapping("/test2")
    @RepeatSubmit(location = "test2", timeout = 10, argName = "random")
    public String test2(@RequestBody TestReq req) throws InterruptedException {
        Thread.sleep(5000);
        return JSON.toJSONString(req);
    }

}

入参

import lombok.Data;

/**
 * @author kwin
 * @Date 2022/1/17 13:48
 **/
@Data
public class TestReq {

    private String test;

    private String test2;

    private String random;
}

结果如下:

java 防止接口并发 防止接口重复调用_java_03

java 防止接口并发 防止接口重复调用_字符串_04

 具体可以根据自己的业务需求和项目需求进行一些更复杂的开发。