数据重复提交解决方案

当用户在新增一条数据时,快速点击多次提交按钮,若没有做拦截,那么将会导致新增多条无意义的数据

本文参考其他文章编写:原文地址1原文地址2

解决方案

方法1. 前端点击提交后将按钮禁用

document.getElementById("btn").disabled = true;

:无法避免恶意用户调用接口提交数据,比如直接请求接口,并不通过前端页面

方法2.在Java代码增加synchronized关键字使提交数据一条一条执行

public synchronized Result<?> save (Model model){ return null }

:使用起来比较简单但性能很低,在处理数据时会有排队等候的问题,并且在多实例部署时无法起到作用
方法3.使用Redis结合拦截器进行拦截(推荐)
参考文章

  • 引入pom
<!--SpringBoot的AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 自定义注解
import java.lang.annotation.*;


/**
 * 放置用户短时间内对数据进行重复提交
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AvoidReSubmit {

    /**
     * 失效时间,即可以第二次提交间隔时长
     * 默认3秒
     * 单位毫秒
     * @return
     */
    long expireTime() default 30 * 1000L; 
}

  • 编写拦截器

import com.hk.frame.util.AvoidReSubmit;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;


import javax.annotation.Resource;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;

/**
接口数据重复提交拦截器
 **/
@Aspect
@Component
public class AvoidReSubmitAspect  {
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 定义匹配规则,以便于后续拦截直接拦截submit方法,不用重复表达式
     * 
     * 此处的参数为注解包名:com.hk.frame.util.AvoidReSubmit
     */
    @Pointcut(value = "@annotation(com.hk.frame.util.AvoidReSubmit)")
    public void submit() {
    }

    @Before("submit()&&@annotation(avoidReSubmit)")
    public void doBefore(JoinPoint joinPoint, AvoidReSubmit avoidReSubmit) {

        // 拼装参数
        StringBuffer sb = new StringBuffer();
        for(Object object : joinPoint.getArgs()){
        	//此处的参数全部为接口请求传递的参数
            sb.append(object);
        }

        String key = md5(sb.toString());
        long expireTime = avoidReSubmit.expireTime();
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Object object = valueOperations.get(key);
        if(null != object){
            throw new RuntimeException("您已经提交了请求,请不要重复提交哦!");
        }
        //使用通过参数生成的key,存放一个值为1,过期时间为注解中指定时间,到redis
        valueOperations.set(key, "1", expireTime, TimeUnit.MILLISECONDS);
    }

    @Around("submit()&&@annotation(avoidReSubmit)")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint, AvoidReSubmit avoidReSubmit) throws Throwable {
   
        System.out.println("环绕通知:");
        Object result = null;
        result = proceedingJoinPoint.proceed();
        //此处的result为拦截的方法,return中的值
        return result;
    }

    @After("submit()")
    public void doAfter() {
        System.out.println("******拦截后的逻辑******");
    }

	/**
	将参数生成为一个不重复的key
	*/
    private String md5(String str){
        if (str == null || str.length() == 0) {
            throw new IllegalArgumentException("String to encript cannot be null or zero length");
        }
        StringBuffer hexString = new StringBuffer();
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[] hash = md.digest();
            for (int i = 0; i < hash.length; i++) {
                if ((0xff & hash[i]) < 0x10) {
                    hexString.append("0" + Integer.toHexString((0xFF & hash[i])));
                } else {
                    hexString.append(Integer.toHexString(0xFF & hash[i]));
                }
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return hexString.toString();
    }

}

  • 使用
    /**
     * 添加数据
     *
     * @param sg
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/add", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
    @AvoidReSubmit(expireTime = 1000 * 3)//3秒内无法重复请求
    public Object addShoppingGuide(@RequestBody Map<String,Object> param) {
        return coas.add(param);
    }

*注:此处的接口参数若为一个实体类,那么该实体类最好使用lombok注解,或者在不使用lombok的情况下生成toString方法。否则拦截将不生效*
例如:使用lombok

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String id;
    private String name;
}

例如:不使用lombok

public class User {
    private String id;
    private String name;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

接口拦截:

    /**
     * 添加用户信息
     *
     此处使用的是user实体类,所以实体类中必须包含toString方法,否则拦截无效
     * @param user
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/addUser", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
    @AvoidReSubmit(expireTime = 1000 * 3)
    public Map<String, Object> addSceneSignInRecord(@RequestBody User user) {
return null;
}

注:若按照以上方法并未生效,可尝试延长(expireTime)时间

方法4.通过数据库唯一约束进行限制,再从代码中捕捉异常

  • 给某个指定字段添加唯一约束
ALTER TABLE 表名  ADD unique(列名);

适用于,比如user表中只希望userName一样时阻止新增

  • 设置联合唯一索引
ALTER TABLE 表名 ADD UNIQUE KEY(列名1,列名2);
  • 设置联合唯一索引并设置索引名称
alter table  表名 add unique key `索引名称` (列名1,列名2);

适用于,比如user表中,希望当userName和age都一样时阻止新增

补充
mysql 查询唯一索引

show index from 表名;

mysql删除唯一索引

alter table 表名 drop index 索引名称;

版权声明:本文为qq_46122292原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。