好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交

一、前言

在面试中,经常会有一道经典面试题,那就是:怎么防止接口重复提交?小编也是背过的,好几种方式,但是一直没有实战过,做多了管理系统,发现这个事情真的没有过多的重视。最近在测试过程中,发现了多次提交会保存两条数据,进而导致程序出现问题!

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

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

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

二、什么是幂等性

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

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

三、REST风格与幂等性

以常用的四种来分析哈!

REST

是否支持幂等

SQL例子

GET

SELECT * FROM table WHER id = 1

PUT

UPDATE table SET age=18 WHERE id = 1

DELETE

DELETE FROM table WHERE id = 1

POST

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 :  8087   spring :  redis :  host :  localhost   port :  6379   password :  123456   datasource :  #使用阿里的Druid   type :  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   */   @Configuration   public   class   RedisConfig  {  @Bean   @SuppressWarnings ( value   =  {  "unchecked" ,  "rawtypes"  })  public   RedisTemplate  <  Object ,  Object  >   redisTemplate ( RedisConnectionFactory   connectionFactory )
    {  RedisTemplate  <  Object ,  Object  >   template   =   new   RedisTemplate  <> ();  template . setConnectionFactory ( connectionFactory );  Jackson2JsonRedisSerializer   serializer   =   new   Jackson2JsonRedisSerializer ( Object . class );  // 使用StringRedisSerializer来序列化和反序列化redis的key值   template . setKeySerializer ( new   StringRedisSerializer ());  template . setValueSerializer ( serializer );  // Hash的key也采用StringRedisSerializer的序列化方式   template . setHashKeySerializer ( new   StringRedisSerializer ());  template . setHashValueSerializer ( serializer );  template . afterPropertiesSet ();  return   template ;
    }
}

4、自定义注解

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

5、编写切片

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

 /**   * @author wangzhenjun   * @date 2022/11/16 8:54   */   @Slf4j   @Component   @Aspect   public   class   RepeatSubmitAspect  {  @Autowired   private   RedisTemplate   redisTemplate ;  /**   * 定义切点   */   @Pointcut ( "@annotation(com.example.demo.annotation.RepeatSubmit)" )  public   void   repeatSubmit () {}  @Around ( "repeatSubmit()" )  public   Object   around ( ProceedingJoinPoint   joinPoint )  throws {  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 )) {  throw   new   RuntimeException ( "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 );  throw   new   Throwable ( throwable );
            }
        }  else  {  // 抛出异常   throw   new   Throwable ( "请勿重复提交" );
        }
    }
}

6、统一返回值

 @Data   @NoArgsConstructor   @AllArgsConstructor   public   class   Result  <  T  >  {  private   Integer   code ;  private   String   msg ;  private   T   data ;  //成功码   public   static   final   Integer   SUCCESS_CODE   =   200 ;  //成功消息   public   static   final   String   SUCCESS_MSG   =   "SUCCESS" ;  //失败   public   static   final   Integer   ERROR_CODE   =   201 ;  public   static   final   String   ERROR_MSG   =   "系统异常,请联系管理员" ;  //没有权限的响应码   public   static   final   Integer   NO_AUTH_COOD   =   999 ;  //执行成功   public   static   <  T  >   Result  <  T  >   success ( T   data ){  return   new   Result  <> ( SUCCESS_CODE , SUCCESS_MSG , data );
    }  //执行失败   public   static   <  T  >   Result   failed ( String   msg ){  msg   =   StringUtils . isEmpty ( msg ) ?   ERROR_MSG  :  msg ;  return   new   Result ( ERROR_CODE , msg , "" );
    }  //传入错误码的方法   public   static   <  T  >   Result   failed ( int   code , String   msg ){  msg   =   StringUtils . isEmpty ( msg ) ?   ERROR_MSG  :  msg ;  return   new   Result ( code , msg , "" );
    }  //传入错误码的数据   public   static   <  T  >   Result   failed ( int   code , String   msg , T   data ){  msg   =   StringUtils . isEmpty ( msg ) ?   ERROR_MSG  :  msg ;  return   new   Result ( code , msg , data );
    }
}

7、简单的全局异常处理

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

/**
 * @author wangzhenjun
 * @date 2022/11/17 15:33
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Throwable.class)
    public Result handleException(Throwable throwable){
        log.error("错误",throwable);
        return Result.failed(500, throwable.getCause().getMessage());
    }
}

8、controller测试

 /**   * @author wangzhenjun   * @date 2022/10/26 16:51   */   @RestController   @RequestMapping ( "/test" )  public   class   TestController  {  @Autowired   private   SysLogService   sysLogService ;  // 默认1s,方便测试查看,写10s   @RepeatSubmit ( expireTime   =   10 )  @PostMapping ( "/saveSysLog" )  public   Result   saveSysLog ( @RequestBody   SysLog   sysLog ){  return   Result . success ( sysLogService . saveSyslog ( sysLog ));
    }
}

9、service

 /**   * @author wangzhenjun   * @date 2022/11/10 16:45   */   @Service   public   class   SysLogServiceImpl   implements   SysLogService  {  @Autowired   private   SysLogMapper   sysLogMapper ;  @Override   public   int   saveSyslog ( SysLog   sysLog ) {  return   sysLogMapper . insert ( sysLog );
    }
}

六、测试

1、postman进行测试

输入请求:
http://localhost:8087/test/saveSysLog请求参数:

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

发送请求两次:

2、查看数据库

只会有一条保存成功!

3、查看redisKey

在10s会自动删除,就可以在次提交!

4、控制台

七、总结

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

原文地址:https://mp.weixin.qq测试数据/s/V9RVXHC45RQA2ZUbpcEnUQ

查看更多关于SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交的详细内容...

  阅读:15次