前言在实际的开发项目中,一个暴露的接口往往会面临很多请求。解释一下幂等性的概念:任意多次执行的影响和一次执行的影响是一样的。按照这个意思,最终的意思就是对数据库的影响只能是一次性的,不能重复。如何保证其幂等性通常有以下手段:数据库建立唯一索引,可以保证只有一个数据令牌机制最终插入到数据库中。每次接口请求前,先获取一个token,然后在下一次请求时,在request的headerbody中加入这个token,在后台进行校验。如果校验通过,删除token,下次请求会重新判断token悲观锁还是乐观锁,悲观锁可以保证其他SQL不能每次更新数据进行update(在数据库引擎是innodb中,select条件必须做一个唯一索引,防止锁住整张表)先查询再判断。首先检查数据库中是否有数据。如果存在,则证明被请求过,直接拒绝请求。如果不存在,就证明是第一次进来,直接让走。redis实现自动幂等性示意图:img搭建redis服务Api首先是搭建一个redis服务器。也可以在springboot中引入redis状态,或者Spring封装的jedis。后面主要用到的API是它的set方法和exists方法。这里使用springboot封装的redisTemplate/***redis工具类*/@ComponentpublicclassRedisService{@AutowiredprivateRedisTemplateredisTemplate;/***写缓存*@paramkey*@paramvalue*@return*/publicbooleanset(finalStringkey,Objectvalue){booleanresult=false;try{ValueOperationsoperations=redisTemplate.opsForValue();operations.set(key,value);result=true;}catch(Exceptione){e.printStackTrace();}returnresult;}/***写缓存设置老化时间*@paramkey*@paramvalue*@return*/publicbooleansetEx(finalStringkey,Objectvalue,LongexpireTime){booleanresult=false;try{ValueOperationsoperations=redisTemplate.opsForValue();operations.set(key,value);redisTemplate.expire(key,expireTime,TimeUnit.SECONDS);result=true;}catch(Exceptione){e.printStackTrace();}returnresult;}/***判断缓存中是否有对应的值*@paramkey*@return*/publicbooleanexists(finalStringkey){returnredisTemplate.hasKey(key);}/***读取缓存*@paramkey*@return*/publicObjectget(finalStringkey){Objectresult=null;ValueOperationsoperations=redisTemplate.opsForValue();result=operations.get(key);returnresult;}/***删除对应的值*@paramkey*/publicbooleanremove(finalStringkey){if(exists(key)){Booleandelete=redisTemplate.delete(key);returndelete;}returnfalse;}}自定义注解AutoIdempotent自定义一个注解,定义这个主要用途注解的作用是将其添加到需要幂等的方法中。每当一个方法被注释时,它就会自动幂等。如果后台使用反射扫描这个注解,就会对这个方法进行处理,实现自动幂等。使用元注解ElementType.METHOD表示只能放在方法上,ettentionPolicy.RUNTIME表示在运行时创建和验证token服务接口@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceAutoIdempotent{}token:我们创建一个新接口并创建一个令牌服务。里面主要有两个方法,一个是用来创建token的,一个是用来验证token的。创建token主要生成字符串,检查token主要传递请求对象。为什么我们需要传递请求对象?主要功能是获取header中的token,然后进行校验,通过抛出Exception获取具体错误信息返回给前端publicinterfaceTokenService{/***createtoken*@return*/publicStringcreateToken();/***checktoken*@paramrequest*@return*/publicbooleancheckToken(HttpServletRequestrequest)throwsException;}token的服务实现类:token是指redis服务,创建token使用随机算法工具类生成随机uuid字符串,然后放入放入redis(为防止数据冗余保留,这里设置过期时间为10000秒,视业务而定),如果输入成功,最后返回token值。checkToken方法是从header中获取token值(如果无法从header中获取,则从paramter中获取),如果不存在则直接抛出异常。这个异常信息可以被拦截器捕获然后返回给前端。@ServicepublicclassTokenServiceImplimplementsTokenService{@AutowiredprivateRedisServiceredisService;/***创建令牌**@return*/@OverridepublicStringcreateToken(){Stringstr=RandomUtil.randomUUID();StrBuildertoken=newStrBuilder();try{token.append(Constant.Redis.TOKEN_PREFIX)。append(str);redisService.setEx(token.toString(),token.toString(),10000L);booleannotEmpty=StrUtil.isNotEmpty(token.toString());if(notEmpty){returntoken.toString();}}catch(Exceptionex){ex.printStackTrace();}returnnull;}/***检验令牌**@paramrequest*@return*/@OverridepublicbooleancheckToken(HttpServletRequestrequest)throwsException{Stringtoken=request.getHeader(Constant.TOKEN_NAME);if(StrUtil.isBlank(token)){//header中不存在于tokentoken=request.getParameter(Constant.TOKEN_NAME);if(StrUtil.isBlank(token)){//parameter中不存在于tokenthrownewServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT,100);}}if(!redisService.exists(token)){thrownewServiceException(Constant.ResponseCode.REPETITIVE_OPERATION,200);}booleanremove=redisService.remove(token);if(!remove){thrownewServiceException(Constant.ResponseCode.REPETITIVE_OPERATION,200);}returntrue;}}拦截器配置web配置类实现WebMvcConfigurerAdapter,主要作用是添加autoIdempotentInterceptor到配置类,这样我们才能让拦截器生效,注意@Configuration注解,这样可以在容器启动的时候添加到context中@ConfigurationpublicclassWebConfigurationextendsWebMvcConfigurerAdapter{@ResourceprivateAutoIdempotentInterceptorautoIdempotentInterceptor;/***添加拦截器*@paramregistry*/@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(autoIdempotentInterceptor);super.addInterceptors(registry);}}拦截处理器:主要作用是拦截扫描到AutoIdempotent的注解方法,然后调用checkToken()tokenService方法验证token是否正确吨?如果捕获到异常,会将异常信息渲染成json返回给前端返回*@throwsException*/@OverridepublicbooleanpreHandle(HttpServletRequestrequest,httpservletResponseresponse,objecthandler)throwsexception{if(!(!);if(methodAnnotation!=null){try{returntokenService.checkToken(request);//幂等验证,验证通过则释放,验证失败则抛出异常,友情提示将通过统一的异常处理返回}catch(Exceptionex){ResultVofailedResult=ResultVo.getFailedResult(101,ex.getMessage());writeReturnJson(response,JSONUtil.toJsonStr(failedResult));throwex;}}//必须返回true,否则所有请求都会被拦截returntrue;}@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{}/***返回的json值*@@ateparamresponse*@ateparamresponsernJson(HttpServletResponseresponse,Stringjson)throwsException{PrintWriterwriter=null;response.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=utf-8");try{writer=response.getWriter();writer.print(json);}catch(IOExceptione){}finally{if(writer!=null)writer.close();}}}测试用例模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法获取具体的token,然后我们调用testIdempotence方法,这个方法注解了@AutoIdempotent,拦截器会拦截所有的请求。当判断处理方法上有这样的注解时,就会调用TokenService。checkToken()方法,如果捕获到异常,它会将异常抛给调用者。让我们模拟请求:@RestControllerpublicclassBusinessController{@ResourceprivateTokenServicetokenService;@ResourceprivateTestServicetestService;@PostMapping("/get/token")publicStringgetToken(){Stringtoken=tokenService.createToken();if(StrUtil.isNotEmpty(token)){ResultVoresultVo=newResultVo();resultVo.setCode(Constant.code_success);resultVo.setMessage(Constant.SUCCESS);resultVo.setData(token);returnJSONUtil.toJsonStr(resultVo);}returnStrUtil.EMPTY;}@AutoIdempotent@PostMapping("/test/Idempotence")publicStringtestIdempotence(){StringbusinessResult=testService.testIdempotence();if(StrUtil.isNotEmpty(businessResult)){ResultVosuccessResult=ResultVo.getSuccessResult(businessResult);returnJSONUtil.toJsonStr(successResult);}returnStrUtil.EMPTY;}}使用postman请求,首先访问get/token路径获取具体token:img使用获取到的token,然后放入具体请求的header中。可以看到第一次请求成功,然后我们请求第二次:img的第二次请求,返回是重复操作,可以看到重复验证通过,多次请求时,我们只让它第一次成功,第二次是失败:img总结了这篇博客的介绍为了使用springboot,拦截器,redis优雅的实现接口幂等,实际开发中幂等性很重要process,因为一个接口可能会被无数个client调用,如何保证不影响后台业务的处理,如何保证只影响数据一次是很重要的。可以防止脏数据或者乱数据,也可以降低并发量。这真是一件非常有益的事情。传统的方法是每次都判断数据。方法不够智能化和自动化,比较麻烦。今天的自动化处理还可以提高程序的可扩展性。