防重复提交实现方案
在WEB系统操作中,往往会出现用户连续重复点击一个按钮导致重复提交,后台程序的同一个接口代码往往上一个请求还没执行完,下一个请求就到达了,而这两个请求又是请求和操作的同一条数据,就会出现业务上的逻辑错误,往往结果不可预料;
要解决重复提交带来的问题的解决方案有多种,不如网上有很多介绍怎么通过前端页面控制来解决重复提交,当然还有其他方式,这里我采用了通过后台程序代码利用redis做分布式锁的方式来防止重复提交,其思路就是在进入一个后端接口执行前先获取一个分布式锁,如果获取成功则上锁,然后执行业务代码,执行完成后再释放分布式锁;如果获取锁失败则可以认为是重复提交的请求,可以将此请求丢弃掉,其流程图如下:
这里有几个关键点:
1)分布式锁:实现分布式锁的方式也有很多种,这里采用redis来实现;
2)注解方式实现:如果在每个接口的前后都加上一堆防止重复提交的代码无疑是非常糟糕的,代码冗余繁琐不说,而且非常不利于代码的扩展和维护,所以如果能通过一个自定义注解实现防重复提交的控制,只在需要控制重复提交的接口上加上这个注解,这样的代码无疑是非常清爽和好维护的;
另外在实现注解时一般可以使用AOP技术和拦截器技术来实现,但是我个人更喜欢用拦截器来实现,这里我就以拦截器的方式来实现;下面给出关键代码:
1、注解的定义:
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface NoRepeatSubmit { 4 }
2、拦截器的实现:
1 private static final RedisCacheUtils redis = RedisCacheUtils.getInstance(RedisConfigEnum.USER_REPEATSUBMIT_LOCK); 2 3 @Override 4 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 5 HandlerMethod handlerMethod = (HandlerMethod) handler; 6 Class<?> clazz = handlerMethod.getBeanType(); 7 Method method = handlerMethod.getMethod(); 8 if (method.isAnnotationPresent(NoRepeatSubmit.class)) { 9 /** 获取token */ 10 String token = getToken(request, clazz.getName(), method.getName()); 11 if (StringUtils.isBlank(token)) { 12 //token未能获取到,则按照默认处理,并记录日志 13 LOGGER.error("{}.{} 未获取到token", clazz.getName(), method.getName()); 14 return true; 15 } 16 17 LOGGER.info("请求URL:{}", request.getRequestURL()); 18 if (!lockToken(token)) { 19 //重复提交,丢弃处理并记录日志 20 LOGGER.error("{}.{} 重复提交", clazz.getName(), method.getName()); 21 return false; 22 } 23 } 24 25 return true; 26 } 27 28 @Override 29 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 30 HandlerMethod handlerMethod = (HandlerMethod) handler; 31 Class<?> clazz = handlerMethod.getBeanType(); 32 Method method = handlerMethod.getMethod(); 33 if (method.isAnnotationPresent(NoRepeatSubmit.class)) { 34 String token = getToken(request, clazz.getName(), method.getName()); 35 if (StringUtils.isNotBlank(token)) { 36 LOGGER.info("请求URL处理完成:{}", request.getRequestURL()); 37 unLockToken(token); 38 } 39 } 40 } 41 42 /** 43 * 从request中读取出token 44 * 45 * @param request 46 * @return 47 */ 48 private String getToken(HttpServletRequest request, String className, String methodName) { 49 if (request.getCookies() != null) { 50 for (Cookie cookie: request.getCookies()) { 51 if (StringUtils.equalsIgnoreCase(cookie.getName(), TOKEN_KEY)) { 52 String cookieToken = cookie.getValue(); 53 54 return String.format("%s.%s.%s", className, methodName, cookieToken); 55 } 56 } 57 } 58 59 return null; 60 } 61 62 /** 63 * 给token上锁 64 * 65 * @param token 66 */ 67 private boolean lockToken(String token) { 68 return redis.setIfAbsent(token, System.currentTimeMillis() + token); 69 } 70 71 /** 72 * 释放token上的锁 73 * 74 * @param token 75 */ 76 private void unLockToken(String token) { 77 redis.delete(token); 78 }
3、在web.xml中定义拦截器:
1 <mvc:interceptors> 2 <bean class="*.*.*.common.Interceptor.RepeatSubmitInterceptor" /> 3 </mvc:interceptors>
4、在接口方法上添加注解:
1 @RequestMapping(value = "/test.do_",method = {RequestMethod.POST}) 2 @ResponseBody 3 @NoRepeatSubmit 4 public Result<JSONObject> testFunc(){ 5 //......业务代码实现....... 6 }