当用户在新增一条数据时,快速点击多次提交按钮,若没有做拦截,那么将会导致新增多条无意义的数据
解决方案:
方法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 索引名称;