我正在参与「启航方案」

一、前语

在面试中,经常会有一道经典面试题,那便是:怎样避免接口重复提交? 小编也是背过的,好几种方式,可是一向没有实战过,做多了办理体系,发现这个工作真的没有过多的注重。 最近在测验过程中,发现了屡次提交会保存两条数据,从而导致程序出现问题!

问题已经出现我们就处理一下吧!!

==本次处理是关于高并发不高的状况,适用于一般的办理体系,给出的处理方案!!高并发的还是建议加分布式锁!!==

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

二、什么是幂等性

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

防接口重复提交,这是必需要做的一件工作!!

三、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 1;
}

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 Throwable {
        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":"我是测验幂等性的"
}

发送恳求两次:

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

2. 查看数据库

只会有一条保存成功!

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

3. 查看redisKey

在10s会主动删去,就能够在次提交!

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

4. 控制台

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

七、总结

这样就处理了幂等性问题,再也不会有错误数据了,减少了一个bug提交!这是一个都要注重的问题,必需要处理,否则可能会出现问题。

结束撒花,假如对你有协助,还请点个重视哈!!你的支撑是我写作的动力!!!


能够看下一小编的微信大众号,文章首发看,欢迎重视,一起沟通哈!!