在上周发布的TienChin项目视频中,我和大家整理了六种幂等性的解决方案。接口幂等性处理是一个很常见的需求。我们其实在很多项目中都会遇到。今天我们就来看看两个比较简单的实现思路。1.接口幂等性实现方案概述事实上,接口幂等性的实现方案有很多。这里我将两种常见的方案分享给小伙伴们。1.1基于Token本方案基于Token的实现思路非常简单。整个过程分为两步:客户端发送请求,从服务端获取一个Token令牌。每个请求都会获得一个全新的令牌。客户端发送请求时,携带第一步的token。在处理请求之前,它会检查令牌是否存在。成功处理请求后,令牌将被删除。大体思路如上。当然具体实现起来会复杂很多,需要注意的细节也很多。之前宋哥也录制过这个解决方案的视频。你可以参考它并录制两个视频。一种是基于拦截器处理,还有一种是基于AOP切面处理:基于拦截器处理(视频1):基于AOP切面处理(视频2):1.2基于请求参数校验最近在TienChin项目中使用的是另一种这个方案是根据请求参数判断的。如果同一个接口收到的请求参数在短时间内是相同的,那么就认为这是一个重复的请求,被拒绝。大致思路是这样的。与第一种方案相比,第二种方案相对麻烦一些,因为只有一次请求,不需要去服务器端获取token。在高并发环境下,这种方案的优势更加明显。那么今天就和大家聊聊第二种方案的实现,后面在TienChin项目视频中会详细讲解。2、根据请求参数进行验证首先,我们新建一个SpringBoot项目,引入Web和Redis依赖。创建完成后,首先配置Redis的基本信息,如下:spring.redis.host=localhostspring.redis.port=6379spring.redis.password=123为了方便后续的Redis操作,我们先简单封装一下Redis,如下:@ComponentpublicclassRedisCache{@AutowiredpublicRedisTemplateredisTemplate;publicvoidsetCacheObject(finalStringkey,finalTvalue,finalIntegertimeout,finalTimeUnittimeUnit){redisTemplate.opsForValue().set(key,value,timeout,timeUnit);}}publicTgetCacheObject(finalStringkey){ValueOperationsoperation=redisTemplate.opsForValue();返回操作.get(key);}}这个比较简单,一个存储数据,一个读取数据。接下来我们自定义一个注解,在需要幂等的接口上加上这个注解。以后这个接口会自动进行幂等处理。@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRepeatSubmit{/***间隔时间(ms),小于这个时间视为重复提交*/publicintinterval()default5000;/***提示信息*/publicStringmessage()default"不允许重复提交,请稍后再试";}我们使用拦截器来解析这个注解,解析代码如下:publicabstractclassRepeatSubmitInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(handlerinstanceofHandlerMethod){HandlerMethodhandlerMethod=(HandlerMethod)处理程序;方法method=handlerMethod.getMethod();RepeatSubmitannotation=method.getAnnotation(RepeatSubmit.class);if(annotation!=null){if(this.isRepeatSubmit(request,annotation)){Mapmap=newHashMap<>();p.put("状态",500);map.put("msg",annotation.message());response.setContentType("application/json;charset=utf-8");response.getWriter().write(newObjectMapper().writeValueAsString(map));返回假;}}返回真;}else{返回真;}}/***验证重复提交是否由子类实现具体的反重复提交规则**@paramrequest*@return*@throwsException*/publicabstractbooleanisRepeatSubmit(HttpServletRequestrequest,RepeatSubmitannotation);}这个拦截器是一个抽象类,拦截接口方法,然后找到接口上的@RepeatSubmit注解,调用isRepeatSubmit方法来判断数据是否重复提交,这个方法在这里是一个抽象方法。我们需要定义另一个类来继承这个抽象类。在新的子类中,可以有不同的幂等判断逻辑。这里我们是根据URL地址+参数判断是否满足幂等条件:@ComponentpublicclassSameUrlDataInterceptorextendsRepeatSubmitInterceptor{publicfinalStringREPEAT_PARAMS="repeatParams";公共最终字符串REPEAT_TIME="重复时间";publicfinalstaticStringREPEAT_SUBMIT_KEY="REPEAT_SUBMIT_KEY";privateStringheader="授权";@AutowiredprivateRedisCacheredisCache;@SuppressWarnings("unchecked")@OverridepublicbooleanisRepeatSubmit(HttpServletRequestrequest,RepeatSubmitannotation){StringnowParams="";if(requestinstanceofRepeatedlyRequestWrapper){RepeatedlyRequestWrapperrepeatedlyRequest=(RepeatedlyRequestWrapper)请求;尝试{nowParams=repeatedRequest.getReader().readLine();}catch(IOExceptione){e.printStackTrace();}}//body参数为空,获取参数的数据if(StringUtils.isEmpty(nowParams)){try{nowParams=newObjectMapper().writeValueAsString(request.getParameterMap());}赶上(JsonProcessingExceptione){e.printStackTrace();}}MapnowDataMap=newHashMap();nowDataMap.put(REPEAT_PARAMS,nowParams);nowDataMap.put(REPEAT_TIME,System.currentTimeMillis());//请求地址(作为键值存放缓存)Stringurl=request.getRequestURI();//唯一值(如果没有消息头,则使用请求地址)StringsubmitKey=request.getHeader(header);//唯一标识符(指定键+url+消息头)StringcacheRepeatKey=REPEAT_SUBMIT_KEY+url+submitKey;对象sessionObj=redisCache.getCacheObject(cacheRepeatKey);if(sessionObj!=null){MapsessionMap=(Map)sessionObj;if(compareParams(nowDataMap,sessionMap)&&compareTime(nowDataMap,sessionMap,annotation.interval())){返回true;}}redisCache.setCacheObject(cacheRepeatKey,nowDataMap,一个nnotation.interval(),TimeUnit.MILLISECONDS);返回假;}/***判断参数是否相同*/privatebooleancompareParams(MapnowMap,MappreMap){StringnowParams=(String)nowMap.get(REPEAT_PARAMS);StringpreParams=(String)preMap.get(REPEAT_PARAMS);返回nowParams.equals(preParams);}/***判断两次之间的时间间隔*/privatebooleancompareTime(MapnowMap,MappreMap,intinterval){longtime1=(Long)nowMap.get(REPEAT_TIME);longtime2=(Long)preMap.get(REPEAT_TIME);如果((时间1-时间2)<间隔){返回真;}返回假;}}我们来看具体的实现逻辑:首先判断当前请求对象是否为RepeatedlyRequestWrapper,如果是则说明当前请求参数为JSON,然后通过IO流读取参数取出,这块小伙伴应该结合上一篇文章来理解,不然你可能会觉得一头雾水,传送门【JSON数据看了一遍就没了,怎么办?]()如果第一步没有获取到参数,说明参数可能不是json格式,而是key-value格式,然后读取key-value形式的参数,转换成json字符串。接下来构造一个Map,将之前读取的参数和当前时间存入Map中。接下来构造存储在Redis中的数据的key。该密钥由固定前缀+请求URL地址+请求头的认证token组成。这个请求头的token还是很重要的。只有这样才能区分当前用户提交的数据(如果是RESTful接口,那么为了区分,接口的请求方法也可以拼接成key作为参数)。接下来去Redis中获取数据。拿到后比较参数是否相同,是否超时。如果判断没有问题,则返回true,表示已经重复请求。否则返回说明用户是第一次向该接口提交数据或者时间窗已过,则重新将参数字符串缓存到Redis中并返回false,说明请求OK。好了,做完这些,我们最后配置拦截器:@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(repeatSubmitInterceptor).add/**");}}这样我们接口的幂等性就处理好了~需要的时候可以直接在接口上使用:@RestControllerpublicclassHelloController{@PostMapping("/hello")@RepeatSubmit(interval=100000)publicStringhello(@RequestBodyStringmsg){System.out.println("msg="+msg);return"hello";}}好了公众号后台回复RepeatSubmit,可以下载本文源码。