今天早上,新来的同事小王突然问我:“周哥,什么是幂等性啊?”。然后我就跟他解释了一番,幂等性就是说无论你执行几次请求,其结果是一样的。说到了幂等就不得不说重复提交了,你连续点击提交按钮,理论上来说这是同一条数据,数据库应该只能存入一条,而实际上存放了多条,这就违反了幂等性。因此我们就需要做一些处理,来保证连续点击提交按钮后,数据库只能存入一条数据。
防止重复提交的方式很多,这里我就说一下我认为比较好用的一种。
自定义注解 +Aop 实现
我们通过获取用户 ip 及访问的接口来判断他是否重复提交,假如这个 ip 在一段时间内容多次访问这个接口,我们则认为是重复提交,我们将重复提交的请求直接处理即可,不让访问目标接口。
自定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmit {
/**
* 默认 1s 钟以内算重复提交
* @return
*/
long timeout() default 1;}
Aop 处理逻辑
我们将 ip+ 接口地址作为 key,随机生成 UUID 作为 value,存入 redis。每次请求进来,根据 key 查询 redis,如果存在则说明是重复提交,抛出异常,如果不存在,则是正常提交,将 key 存入 redis。
@Aspect
@Component
public class NoRepeatSubmitAop {
@Autowired
private RedisService redisUtils;
/**
* 定义切入点
*/
@Pointcut("@annotation(NoRepeatSubmit)")
public void noRepeat() {}
/**
* 前置通知:在连接点之前执行的通知
* @param point
* @throws Throwable
*/
@Before("noRepeat()")
public void before(JoinPoint point) throws Exception{
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Assert.notNull(request, "request can not null");
// 此处可以用 token 或者 JSessionId
String token = IpUtils.getIpAddr(request);
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
List<Object> lGet = redisUtils.lGet(key, 0, -1);
// 获取注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
long timeout = annotation.timeout();
boolean isSuccess = false;
if (lGet.size()==0 || lGet == null) {isSuccess = redisUtils.lSet(key, clientId, timeout);
}
if (!isSuccess) {
// 获取锁失败,认为是重复提交的请求
redisUtils.lSet(key, clientId, timeout);
throw new Exception("不可以重复提交");
}
}
private String getKey(String token, String path) {return token + path;}
private String getClientId() {return UUID.randomUUID().toString();}
}
提供接口用来测试
在接口上添加上我们自定义的注解 @NoRepeatSubmit
@RequestMapping("/test")
@NoRepeatSubmit
public String tt(HttpServletRequest request) {return "1";}
测试
我们在浏览器中连续请求两次接口。发现第一次接口响应正常内容:1,第二次接口响应了不可重复提交的异常信息。1s 之后再点击接口,发现又响应了正常内容。
至此,这种防止重复提交的方式就介绍完了,这样我们就完美防止了接口重复提交。