当前位置: 首页 > 科技观察

SpringBoot自定义注解+AOP+redis实现防接口幂等重复提交,从概念到实战

时间:2023-03-11 22:24:10 科技观察

1.前言面试中经常出现一道经典的面试题,那就是:如何防止接口重复提交?小编也记住了几种方法,但是一直没有在实战中尝试过。做了很多管理制度,发现这个事情真的没有太重视。最近测试的时候,发现多次提交会保存两条数据,这样会导致程序出现问题!问题出现了,我们来解决吧!!本方案针对高并发但不高的情况,适用于一般管理系统,给出方案!!高并发建议加分布式锁!!先说说什么是幂等?2、什么是幂等性?接口幂等性是指用户对同一操作发起一次请求或多次请求的结果是一致的,不会因为多次点击而产生副作用;比如经典的支付场景:用户购买了商品支付扣费成功,但是返回结果的时候网络异常。这个时候,钱已经被扣了。用户再次点击该按钮,此时会进行第二次扣款。结果返回成功,流水记录也变成了条条,不保证接口的幂等性;可谓是:商家高兴,买家大骂!防止重复提交接口,这个是必须要做的!3.REST风格与幂等性用四种常用类型解析!REST是否支持幂等SQL示例GETisSELECT*FROMtableWHERid=1PUTisUPDATEtableSETage=18WHEREid=1DELETEisDELETEFROMtableWHEREid=1POSTisnotINSERTINTOtable(id,age)VALUES(1,21)那么我们要解决的就是POST请求了!4.解决思路大概是主流的解决方案:token机制(前端在请求头上携带标识,后端校验)锁机制数据库悲观锁(锁表)数据库乐观锁(版本号受控))业务层分布式锁(加分布式锁redisson)全局唯一索引机制redisset机制前端按钮加限制小编的解决方案是redis的set机制!对于同一个用户,任何与POST存储相关的接口在1秒内只能提交一次。完全用后台控制,前端可以限制,但是体验不好!后端使用自定义注解为需要反幂等的接口添加注解,使用AOP切片降低与业务的耦合!获取分片中用户的token、user_id、url,组成redis的唯一键!第一次请求会先判断key是否存在。如果不存在,则在redis中添加一个主键key,并设置过期时间;如果有异常,会主动删除key。如果没有删除失败,等待1s,redis会自动删除。时间错误是可以接受的!第二次请求来的时候先判断key是否存在,如果存在则重复提交,返回保存的信息!5.SpringBoot实际版本为2.7.4。1、导入依赖org.springframework.bootspring-boot-starter-data-redisorg.projectlomboklombok1.18.2org.springframework.bootspring-boot-starter-aoporg.springframework.bootspring-boot-starter-webcom.alibabadruid-spring-boot-starter1.1.16org.springframework.boot</groupId>spring-boot-starter-jdbcmysqlmysql-connector-javacom.baomidoumybatis-plus-boot-starter3.5.1org.springframework.bootspring-boot-starter-test测试2、编写ymlserver:port:8087spring:redis:host:localhostport:6379password:123456datasource:#使用阿里的Druidtype:com.alibaba.druid.pool.DruidDataSourcedriver-class-name:com.mysql.cj。jdbc.Driverurl:jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC用户名:root密码:3、redis序列化/***@authorwangzhenjun*@date2022/11/1715:20*/@ConfigurationpublicclassRedisConfig{@Bean@SuppressWarnings(value={"unchecked","rawtypes"})publicRedisTemplateredisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplatetemplate=newRedisTemplate<>();}template.setConnectionFactory(connectionFactory);Jackson2JsonRedisSerializer序列化器=newJackson2JsonRedisSerializer(StringRedisSerializer);//使用来序列化和反序列化redis的键值template.setKeySerializer(newStringRedisSerializer());template.setValueSerializer(序列化器);//Hashkey也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(newStringRedisSerializer());template.setHashValueSerializer(序列化器);模板.afterPropertiesSet();返回模板;}}4.自定义注解/***自定义注解防止重复提交表单*@a作者wangzhenjun*@date2022/11/1715:18*/@Target(ElementType.METHOD)//注解只能用于方法@Retention(RetentionPolicy.RUNTIME)//修改注解的生命周期@Documentedpublic@interfaceRepeatSubmit{/***反重复操作过期时间,默认1s*/longexpireTime()default;}5.写入分片异常信息,替换成你想抛出的异常。这里不详细划分异常,仅供博客记录不完美的项目哈!/***@authorwangzhenjun*@date2022/11/168:54*/@Slf4j@Component@AspectpublicclassRepeatSubmitAspect{@AutowiredprivateRedisTemplateredisTemplate;/***定义切点*/@Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")publicvoidrepeatSubmit(){}@Around("repeatSubmit()")publicObjectaround(ProceedingJoinPointjoinPoint)抛出{ServletRequestAttributes属性=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest请求=属性。获取请求();方法method=((MethodSignature)joinPoint.getSignature())。获取方法();//获取防重复提交注解RepeatSubmitannotation=method.getAnnotation(RepeatSubmit.class);//获取token作为key。这里新建后端项目获取不到,所以先写死//Stringtoken=request.getHeader("Authorization");StringtokenKey="hhhhhhh,你好";if(StringUtils.isBlank(token)){thrownewRuntimeException("Token不存在,请登录!");}字符串url=request.getRequestURI();/***在redis上使用prefix+url+token生成key*可以添加用户id,我这里获取不到,可以添加到项目中*/StringredisKey="repeat_submit_key:".concat(网址).concat(tokenKey);log.info("==========redisKey======{}",redisKey);如果(!redisTemplate.hasKey(redisKey)){redisTemplate。opsForValue().set(redisKey,redisKey,annotation.expireTime(),TimeUnit.SECONDS);try{//正常执行方法并返回returnjoinPoint.proceed();}catch(Throwablethrowable){重新disTemplate.delete(redisKey);抛出新的可抛物(可抛物);}}else{//抛出异常thrownewThrowable("不要重复提交");}}}6.统一返回值@Data@NoArgsConstructor@AllArgsConstructorpublicclassResult{privateIntegercode;私有字符串消息;私有T数据;//成功代码publicstaticfinalIntegerSUCCESS_CODE=200;//成功信息publicstaticfinalStringSUCCESS_MSG="SUCCESS";//失败publicstaticfinalIntegerERROR_CODE=201;publicstaticfinalStringERROR_MSG="系统异常,请联系管理员";//无权限响应码publicstaticfinalIntegerNO_AUTH_COOD=999;//成功执行publicstaticResultsuccess(Tdata){returnnewResult<>(SUCCESS_CODE,SUCCESS_MSG,data);}//执行失败publicstaticResultfailed(Stringmsg){msg=StringUtils.isEmpty(msg)?ERROR_MSG:味精;返回新结果(ERROR_CODE,msg,"");}//传入错误码p的方法publicstatic结果失败(intcode,Stringmsg){msg=StringUtils.isEmpty(msg)?ERROR_MSG:味精;返回新的结果(代码,味精,“”);}//传入错误码的数据publicstaticResultfailed(intcode,Stringmsg,Tdata){msg=StringUtils.isEmpty(msg)?ERROR_MSG:味精;返回新结果(代码、消息、数据);}}7.简单的全局异常处理这是一个不完整的版本,请勿模仿!/***@authorwangzhenjun*@date2022/11/1715:33*/@Slf4j@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(value=Throwable.class)publicResulthandleException(Throwablethrowable){log.error("错误“,可抛出的);返回Result.failed(500,throwable.getCause().getMessage());}}8、控制器测试/***@authorwangzhenjun*@date2022/10/2616:51*/@RestController@RequestMapping("/test")publicclassTestController{@AutowiredprivateSysLogServicesysLogService;//默认1s,方便测试查看,写10s@RepeatSubmit(expireTime=10)@PostMapping("/saveSysLog")publicResultsaveSysLog(@RequestBodySysLogsysLog){returnResult.success(sysLogService.saveSyslog(sysLog));}}9、service/***@authorwangzhenjun*@date2022/11/1016:45*/@ServicepublicclassSysLogServiceImplimplementsSysLogService{@AutowiredprivateSysLogMappersysLogMapper;@OverridepublicintsaveSyslog(SysLogsysLog){returnsysLogMapper.insert(sysLog);}}6.测试1.Postman执行测试输入请求:http://localhost:8087/test/saveSysLog请求参数:{"title":"Hello","method":"post","operName":"Iamtestingidempotency"}两次发送请求:2.View中只有一项数据库将保存成功!3.查看redisKey是否会在10秒后自动删除,可以再次提交!4.控制台7.总结这样就解决了幂等性的问题,不会再有错误的数据了,reducing提交了bug!这是大家要注意的问题,一定要解决,否则可能会出现问题莱姆斯