当前位置: 首页 > 后端技术 > Java

SpringBoot+Redis解决了重复提交的问题,还有谁不会呢??

时间:2023-04-02 00:20:57 Java

作者|慕容倩语\来源:https://www.jianshu.com/p/c80...前言在实际的开发项目中,一个对外暴露的接口,往往会面临很多请求。下面我们来解释一下幂等性的概念:任意多次执行的影响与一次执行的影响是一样的。按照这个意思,最终的意思就是对数据库的影响只能是一次性的,不能重复。如何保证其幂等性通常有以下几种手段:1、数据库建立唯一索引,可以保证最终只有一条数据插入到数据库中。2.代币机制。在每次接口请求前获取一个token,然后在下次请求时将这个token添加到请求的headerbody中,在后台进行校验。如果验证通过,删除token,下次请求会重新判断token。3.悲观锁或乐观锁,悲观锁可以保证其他SQL不能每次更新数据进行update(当数据库引擎为innodb时,select条件必须是唯一索引,防止锁住整张表)4.先查询再查询然后判断,首先查看数据库中是否有数据,如果存在,则证明请求已请求,直接拒绝请求;如果不存在,证明是第一次进来,直接放行。Redis实现自动幂等性示意图:搭建Redis服务API1首先,搭建一个redis服务器。2、也可以在springboot中引入redis的stater,或者Spring封装的jedis。后面主要用到的API是它的set方法和exists方法。这里我们使用springboot封装的redisTemplate。推荐一个SpringBoot基础教程和实例:https://github.com/javastacks.../***redis工具类*/@ComponentpublicclassRedisService{@AutowiredprivateRedisTemplateredisTemplate;/***写入缓存*@paramkey*@paramvalue*@return*/publicbooleanset(finalStringkey,Objectvalue){booleanresult=false;尝试{ValueOperationsoperations=redisTemplate.opsForValue();操作集(键,值);结果=真;}catch(Exceptione){e.printStackTrace();}返回结果;}/***写缓存设置老化时间*@paramkey*@paramvalue*@return*/publicbooleansetEx(finalStringkey,Objectvalue,LongexpireTime){booleanresult=false;尝试{ValueOperationsoperations=redisTemplate.opsForValue();操作集(键,值);te.expire(key,expireTime,TimeUnit.SECONDS);结果=真;}catch(Exceptione){e.printStackTrace();}返回结果;}/***判断缓存中是否有对应的值*@paramkey*@return*/publicbooleanexists(finalStringkey){returnredisTemplate.hasKey(key);}/***读取缓存*@paramkey*@return*/publicObjectget(finalStringkey){Objectresult=null;ValueOperationsoperations=redisTemplate.opsForValue();结果=operations.get(key);返回结果;}/***删除对应的值*@paramkey*/publicbooleanremove(finalStringkey){if(exists(key)){Booleandelete=redisTemplate.delete(key);返回删除;}返回假;}}自定义注解AutoIdempotent自定义一个注解。定义这个注解的主要目的是在需要实现幂等的时候添加到方法中,任何方法注解它都会实现自动幂等如果后台使用反射扫描这个注解,就会对这个方法进行处理,实现自动幂等。使用元注解ElementType.METHOD表示只能放在方法上,attentionPolicy.RUNTIME表示正在运行。@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceAutoIdempotent{}token创建和验证token服务接口:我们新建一个创建token服务的接口,主要包含两个方法,一个是用来创建令牌,用于验证的令牌。创建token主要生成字符串,检查token主要传递请求对象。为什么我们需要传递请求对象?主要作用是获取header中的token,然后进行校验,通过抛出的Exception获取具体的错误信息返回给前端。publicinterfaceTokenService{/***创建令牌*@return*/publicStringcreateToken();/***检查token*@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();尝试{token.append(Constant.Redis.TOKEN_PREFIX).append(str);redisService.setEx(token.toString(),token.toString(),10000L);booleannotEmpty=StrUtil.isNotEmpty(token.toString());如果(notEmpty){返回token.toString();}}catch(Exceptionex){ex.printStackTrace();}返回空;}/***检查令牌**@paramrequest*@return*/@OverridepublicbooleancheckToken(HttpServletRequestrequest)throwsException{Stringtoken=request.getHeader(Constant.TOKEN_NAME);if(StrUtil.isBlank(token)){//标头令牌中不存在令牌=request.getParameter(Constant.TOKEN_NAME);if(StrUtil.isBlank(token)){//参数中不存在令牌thrownewServiceException(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);返回真;}}拦截器配置web配置类,实现WebMvcConfigurerAdapter,主要作用是在配置类中添加autoIdempotentInterceptor,这样我们就可以让拦截器生效了,注意使用@Configuration注解,这样可以添加到容器启动时的上下文@ConfigurationpublicclassWebConfigurationextendsWebMvcConfigurerAdapter{@ResourceprivateAutoIdempotentInterceptorautoIdempotentInterceptor;/***添加拦截器*@paramregistry*/@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(autoIdempotentInterceptor);super.addInterceptors(注册表);}}拦截处理器:主要作用是拦截AutoIdempotent到注解方法,然后调用tokenService的checkToken()方法验证token是否正确。如果捕获到异常,会将异常信息渲染成json返回给前端。/***拦截器*/@ComponentpublicclassAutoIdempotentInterceptorimplementsHandlerInterceptor{@AutowiredprivateTokenServicetokenService;/***预处理**@paramrequest*@paramresponse*@paramhandler*@return*@throwsException*/@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(!(handlerinstanceofHandlerMethod)){returntrue;}HandlerMethodhandlerMethod=(HandlerMethod)处理程序;标注扫描AutoIdempotentmethodAnnotation=method.getAnnotation(AutoIdempotent.class);if(methodAnnotation!=null){try{returntokenService.checkToken(request);//幂等性验证,验证通过则释放,验证失败则抛出异常,并返回友好提示通过统一的异常处理}catch(Exceptionex){ResultVofailedResult=ResultVo.getFailedResult(101,ex.getMessage());writeReturnJson(响应,JSONUtil.toJsonStr(failedResult));扔前;}}//必须返回true,否则会被截断请求returntrue;}@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{}/***返回的json值*@paramresponse*@paramjson*@throwsException*/privatevoidwriteReturnJson(HttpServletResponseresponse,Stringjson)throwsException{PrintWriterwriter=null;response.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=utf-8");尝试{writer=response.getWriter();writer.print(json);}抓住(IOExceptione){}最后{if(writer!=null)writer.close();}}}测试用例模拟业务请求类,首先我们需要通过/get/token路径,通过getToken()方法获取具体的token,然后调用testIdempotence方法,该方法注解为@AutoIdempotent,以及拦截器将拦截所有请求。当判断处理方法有注解时,会调用TokenService中的checkToken()方法。如果捕获到异常,就会抛给调用者。让我们模拟请求:@RestControllerpublicclassBusinessController{@ResourceprivateTokenServicetokenService;@Resource私有测试服务测试服务;@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);返回JSONUtil.toJsonStr(resultVo);}返回StrUtil.EMPTY;}@AutoIdempotent@PostMapping("/test/Idempotence")公共字符串testIdempotence(){StringbusinessResult=testService.testIdempotence();如果(StrUtil.isNotEmpty(businessResult)){ResultVosuccessResult=ResultVo.getSuccessResult(businessResult);返回JSONUtil.toJsonStr(successResult);}返回StrUtil.EMPTY;}}使用postman请求,先访问get/token路径获取具体的token:使用获取到的token,然后放到具体的请求头中,可以看到第一次请求成功,然后我们请求第二次time:第二次请求,返回是重复操作,可以看出重复验证通过,多次请求时,我们只让它第一次成功,第二次失败:总结这个博客介绍了springboot、拦截器、redis的使用要优雅接口幂等性的实现在实际开发过程中对于幂等性非常重要,因为一个接口可能会被无数个客户端调用,如何保证不影响后台业务处理,如何保证只对数据产生一次影响是非常重要的,可以防止脏数据或者乱数据的产生,也可以减少并发量,非常有好处。传统的方法是每次都要判断数据,不够智能化和自动化,而且比较麻烦。今天的自动化处理还可以提高程序的可扩展性。近期热点文章推荐:1.1,000+Java面试题及答案(2021最新版)2.别在满屏的if/else中,试试策略模式,真的很好吃!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.6正式发布,一大波新特性。.5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!