0


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

一、前言

在面试中,经常会有一道经典面试题,那就是:

怎么防止接口重复提交?

小编也是背过的,好几种方式,但是一直没有实战过,做多了管理系统,发现这个事情真的没有过多的重视。
最近在测试过程中,发现了多次提交会保存两条数据,进而导致程序出现问题!

问题已经出现我们就解决一下吧!!

本次解决是对于高并发不高的情况,适用于一般的管理系统,给出的解决方案!!高并发的还是建议加分布式锁!!

下面我们来聊聊幂等性是什么?

二、什么是幂等性

接口幂等性就是用户对于

同一操作

发起的

一次请求或者多次请求

的结果是

一致的

,不会因
为多次点击而产生了副作用;
比如说经典的支付场景:用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了条,这就没有保证接口的幂等性;
可谓:商家美滋滋,买家骂咧咧!!

防接口重复提交,这是必须要做的一件事情!!

三、REST风格与幂等性

以常用的四种来分析哈!
REST是否支持幂等SQL例子GET是SELECT * FROM table WHER id = 1PUT是UPDATE table SET age=18 WHERE id = 1DELETE是DELETE FROM table WHERE id = 1POST否INSERT INTO table (id,age) VALUES(1,21)
所以我们要解决的就是

POST

请求!

四、解决思路

大概主流的解决方案:

  • token机制(前端带着在请求头上带着标识,后端验证)
  • 加锁机制 - 数据库悲观锁(锁表)- 数据库乐观锁(version号进行控制)- 业务层分布式锁(加分布式锁redisson)
  • 全局唯一索引机制
  • redis的set机制
  • 前端按钮加限制

小编的解决方案就是redis的set机制!

同一个用户,任何POST保存相关的接口,1s内只能提交一次。

完全使用后端来进行控制,前端可以加限制,不过体验不好!

后端通过自定义注解,在需要防幂等接口上添加注解,利用AOP切片,减少和业务的耦合!
在切片中获取用户的

token、user_id、url

构成redis的唯一key!
第一次请求会先判断key是否存在,如果不存在,则往redis添加一个主键key,设置过期时间;

如果有异常会主动删除key,万一没有删除失败,等待1s,redis也会自动删除,时间误差是可以接受的!
第二个请求过来,先判断key是否存在,如果存在,则是重复提交,返回保存信息!!

五、实战

SpringBoot版本为

2.7.4

1. 导入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Druid --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.16</version></dependency><!--jdbc--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>

2. 编写yml

server:port:8087spring:redis:host: localhost
    port:6379password:123456datasource:#使用阿里的Druidtype: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
    username: root
    password:

3. redis序列化

/**
 * @author wangzhenjun
 * @date 2022/11/17 15:20
 */@ConfigurationpublicclassRedisConfig{@Bean@SuppressWarnings(value ={"unchecked","rawtypes"})publicRedisTemplate<Object,Object>redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object,Object> template =newRedisTemplate<>();
        template.setConnectionFactory(connectionFactory);Jackson2JsonRedisSerializer serializer =newJackson2JsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(newStringRedisSerializer());
        template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(newStringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();return template;}}

4. 自定义注解

/**
 * 自定义注解防止表单重复提交
 * @author wangzhenjun
 * @date 2022/11/17 15:18
 */@Target(ElementType.METHOD)// 注解只能用于方法@Retention(RetentionPolicy.RUNTIME)// 修饰注解的生命周期@Documentedpublic@interfaceRepeatSubmit{/**
     * 防重复操作过期时间,默认1s
     */longexpireTime()default1;}

5. 编写切片

异常信息大家换成自己想抛的异常,小编这里就没有详细划分异常,就是为了写博客而记录的不完美项目哈!!

/**
 * @author wangzhenjun
 * @date 2022/11/16 8:54
 */@Slf4j@Component@AspectpublicclassRepeatSubmitAspect{@AutowiredprivateRedisTemplate redisTemplate;/**
     * 定义切点
     */@Pointcut("@annotation(com.example.demo.annotation.RepeatSubmit)")publicvoidrepeatSubmit(){}@Around("repeatSubmit()")publicObjectaround(ProceedingJoinPoint joinPoint)throwsThrowable{ServletRequestAttributes attributes =(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Method method =((MethodSignature) joinPoint.getSignature()).getMethod();// 获取防重复提交注解RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);// 获取token当做key,小编这里是新后端项目获取不到哈,先写死// String token = request.getHeader("Authorization");String tokenKey ="hhhhhhh,nihao";if(StringUtils.isBlank(token)){thrownewRuntimeException("token不存在,请登录!");}String url = request.getRequestURI();/**
         *  通过前缀 + url + token 来生成redis上的 key
         *  可以在加上用户id,小编这里没办法获取,大家可以在项目中加上
         */String redisKey ="repeat_submit_key:".concat(url).concat(tokenKey);
        log.info("==========redisKey ====== {}",redisKey);if(!redisTemplate.hasKey(redisKey)){
            redisTemplate.opsForValue().set(redisKey, redisKey, annotation.expireTime(),TimeUnit.SECONDS);try{//正常执行方法并返回return joinPoint.proceed();}catch(Throwable throwable){
                redisTemplate.delete(redisKey);thrownewThrowable(throwable);}}else{// 抛出异常thrownewThrowable("请勿重复提交");}}}

6. 统一返回值

@Data@NoArgsConstructor@AllArgsConstructorpublicclassResult<T>{privateInteger code;privateString msg;privateT data;//成功码publicstaticfinalIntegerSUCCESS_CODE=200;//成功消息publicstaticfinalStringSUCCESS_MSG="SUCCESS";//失败publicstaticfinalIntegerERROR_CODE=201;publicstaticfinalStringERROR_MSG="系统异常,请联系管理员";//没有权限的响应码publicstaticfinalIntegerNO_AUTH_COOD=999;//执行成功publicstatic<T>Result<T>success(T data){returnnewResult<>(SUCCESS_CODE,SUCCESS_MSG,data);}//执行失败publicstatic<T>Resultfailed(String msg){
        msg =StringUtils.isEmpty(msg)?ERROR_MSG: msg;returnnewResult(ERROR_CODE,msg,"");}//传入错误码的方法publicstatic<T>Resultfailed(int code,String msg){
        msg =StringUtils.isEmpty(msg)?ERROR_MSG: msg;returnnewResult(code,msg,"");}//传入错误码的数据publicstatic<T>Resultfailed(int code,String msg,T data){
        msg =StringUtils.isEmpty(msg)?ERROR_MSG: msg;returnnewResult(code,msg,data);}}

7. 简单的全局异常处理

这是残缺版,大家不要模仿!!

/**
 * @author wangzhenjun
 * @date 2022/11/17 15:33
 */@Slf4j@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(value =Throwable.class)publicResulthandleException(Throwable throwable){
        log.error("错误",throwable);returnResult.failed(500, throwable.getCause().getMessage());}}

8. controller测试

/**
 * @author wangzhenjun
 * @date 2022/10/26 16:51
 */@RestController@RequestMapping("/test")publicclassTestController{@AutowiredprivateSysLogService sysLogService;// 默认1s,方便测试查看,写10s@RepeatSubmit(expireTime =10)@PostMapping("/saveSysLog")publicResultsaveSysLog(@RequestBodySysLog sysLog){returnResult.success(sysLogService.saveSyslog(sysLog));}}

9. service

/**
 * @author wangzhenjun
 * @date 2022/11/10 16:45
 */@ServicepublicclassSysLogServiceImplimplementsSysLogService{@AutowiredprivateSysLogMapper sysLogMapper;@OverridepublicintsaveSyslog(SysLog sysLog){return sysLogMapper.insert(sysLog);}}

六、测试

1. postman进行测试

输入请求:

http://localhost:8087/test/saveSysLog

请求参数:

{"title":"你好",
    "method":"post",
    "operName":"我是测试幂等性的"}

发送请求两次:

在这里插入图片描述

2. 查看数据库

只会有一条保存成功!
在这里插入图片描述

3. 查看redisKey

在10s会自动删除,就可以在次提交!
在这里插入图片描述

4. 控制台

在这里插入图片描述

七、总结

这样就解决了幂等性问题,再也不会有错误数据了,减少了一个bug提交!这是一个都要重视的问题,必须要解决,不然可能会出现问题。

完结撒花,如果对你有帮助,还请点个关注哈!!你的支持是我写作的动力!!!


可以看下一小编的微信公众号,和网站文章首发看,欢迎关注,一起交流哈!!

点击访问!小编自己的网站,里面也是有很多好的文章哦!

标签: springboot redis java

本文转载自: https://blog.csdn.net/qq_52423918/article/details/127903111
版权归原作者 掉发的小王 所有, 如有侵权,请联系我们删除。

“SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战”的评论:

还没有评论