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

一、前言

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

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

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

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

二、什么是幂等性

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

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

三、REST风格与幂等性

以常用的四种来分析哈!

REST是否支持幂等SQL例子
GETSELECT * FROM table WHER id = 1
PUTUPDATE table SET age=18 WHERE id = 1
DELETEDELETE FROM table WHERE id = 1
POSTINSERT INTO table (id,age) VALUES(1,21)

所以我们要解决的就是POST请求!

四、解决思路

大概主流的解决方案:

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

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

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

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

后端通过自定义注解,在需要防幂等接口上添加注解,利用切片,减少和业务的耦合!
在切片中获取用户的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

2. 编写yml

server:  port: 8087spring:  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:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3. redis序列化

/** * @author wangzhenjun * @date 2022/11/17 15:20 */@Configurationpublic 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;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

4. 自定义注解

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

5. 编写切片

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

/** * @author wangzhenjun * @date 2022/11/16 8:54 */@Slf4j@Component@Aspectpublic 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("请勿重复提交");        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

6. 统一返回值

@Data@NoArgsConstructor@AllArgsConstructorpublic 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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

7. 简单的全局异常处理

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

/** * @author wangzhenjun * @date 2022/11/17 15:33 */@Slf4j@RestControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(value = Throwable.class)    public Result handleException(Throwable throwable){        log.error("错误",throwable);        return Result.failed(500, throwable.getCause().getMessage());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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));    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

9. service

/** * @author wangzhenjun * @date 2022/11/10 16:45 */@Servicepublic class SysLogServiceImpl implements SysLogService {	@Autowired    private SysLogMapper sysLogMapper;	@Override    public int saveSyslog(SysLog sysLog) {        return sysLogMapper.insert(sysLog);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

六、测试

1. postman进行测试

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

{    "title":"你好",    "method":"post",    "operName":"我是测试幂等性的"}
  • 1
  • 2
  • 3
  • 4
  • 5

发送请求两次:

2. 查看数据库

只会有一条保存成功!

3. 查看redisKey

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

4. 控制台

七、总结

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

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


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

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发