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

news2024/11/16 0:24:05

一、前言

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

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

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

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

二、什么是幂等性

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

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

三、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内只能提交一次。

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

后端通过自定义注解,在需要防幂等接口上添加注解,利用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":"我是测试幂等性的"
}

发送请求两次:

在这里插入图片描述

2. 查看数据库

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

3. 查看redisKey

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

4. 控制台

在这里插入图片描述

七、总结

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

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


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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/12346.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]java毕业设计酒店管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

K8s为何需要Pod

Pod是K8s中的最小API对象&#xff0c;更专业的表述是&#xff0c;Pod是K8s项目的原子调度单位&#xff0c;Pod可以看做是一个进程组&#xff0c;K8s则是操作系统 一&#xff0c; 问题 现在有三个服务需要部署在同一个节点上&#xff0c;A占用1G内存&#xff0c;B &#xff0c;…

【Maven】使用maven profile 动态激活不同环境、依赖打包部署

使用maven profile 动态激活不同环境、依赖打包部署前言一、配置二、激活Profile三、动态依赖 示例一些其他参考&#xff1a;前言 在开发过程中&#xff0c;我们的软件会面对不同的运行环境&#xff0c;比如开发环境、测试环境、生产环境&#xff0c;而我们的软件在不同的环境中…

Java方法与方法重载

目录 如何使用带参数的方法 综合案例 常见错误2-1 常见错误2-2 方法传参 构造方法 构造方法重载 this的用法 方法重载 成员变量和局部变量 成员变量和局部变量的区别 如何使用带参数的方法 1、定义带参数的方法 语法&#xff1a;<访问修饰符> 返回类型 <方…

网络 IO 演变过程

在互联网中提起网络&#xff0c;我们都会避免不了讨论高并发、百万连接。而此处的百万连接的实现&#xff0c;脱离不了网络 IO 的选择&#xff0c;因此本文作为一篇个人学习的笔记&#xff0c;特此进行记录一下整个网络 IO 的发展演变过程。以及目前广泛使用的网络模型。 1.网…

服务端Skynet(一)——源码浅析

服务端Skynet(一)——源码浅析 文章目录服务端Skynet(一)——源码浅析1、skynet的本质2、skynet基本的数据结构1、skynet_modules管理模块2、skynet_context模块3、skynet_message模块3、skynet启动服务步骤4、启动服务例子(logger)参考文献&#xff1a;skynet设计综述 skynet…

实战Netty!基于私有协议,怎样快速开发网络通信服务?

前言 今天我们一起来来聊聊怎么使用netty。 在工作中&#xff0c;我经常使用netty开发一些服务&#xff0c;掌握netty的工作原理&#xff0c;开发一些服务端以及客户端是非常简单&#xff0c;本篇文章&#xff0c;我们就以具体的协议来进行一个简单的服务的开发。 正文 私有…

一键汇总报告模型可能会需要修改的地方

文章目录如何修改文件夹地址为指定地址?如何取消清除提醒, 避免每次点击弹窗选择?如何取消完成弹窗提醒?如果觉得反复打开工作簿太闪怎么办?如果有些报告的内容页不在第一个Sheet怎么办?如果想修改字段怎么办?运行程序前不想清除原有内容怎么办?报告页有内容但是没有被抓…

混合整数规划的机组组合附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Redis常见面试题(2022)

Redis基础 什么是 Redis&#xff1f; Redis 是一个开源&#xff08;BSD 许可&#xff09;、基于内存、支持多种数据结构的存储系统&#xff0c;可以作为数据库、缓存和消息中间件。它支持的数据结构有字符串&#xff08;strings&#xff09;、哈希&#xff08;hashes&#xf…

多分类交叉熵理解

多分类交叉熵有多种不同的表示形式&#xff0c;如下图所示&#xff1a; 但是&#xff0c;有时候我们读论文会深陷其中不能自拔。 也有很多读者、观众会纠正其他作者的文章、视频的交叉熵形式。 实际上&#xff0c;上述三种形式都是没有问题的。 这里&#xff0c;我们就要了解…

多媒体内容理解在美图社区的应用实践

导读&#xff1a;移动互联网时代&#xff0c;图像和短视频等多媒体内容爆发&#xff0c;基于计算机视觉的AI算法是多媒体内容分析的基础。在美图社区智能化发展的过程中&#xff0c;视频和图像分类打标、去重以及质量评估的结果&#xff0c;在推荐、搜索以及人工审核等多个场景…

【R语言数据科学】:变量选择(三)主成分回归和偏最小二乘回归

变量选择(三)主成分回归和偏最小二乘回归 🌸个人主页:JOJO数据科学📝个人介绍:统计学top3高校统计学硕士在读💌如果文章对你有帮助,欢迎✌关注、👍点赞、✌收藏、👍订阅专栏✨本文收录于【R语言数据科学】本系列主要介绍R语言在数据科学领域的应用包括: R语言编…

多分类问题的precision和recall以及F1 scores的计算

对于多分类问题&#xff0c;首先&#xff0c;对于每一个类的精准率&#xff08;Precision&#xff09;和召回率&#xff08;Recall&#xff09;&#xff0c;定义和二分类问题一致&#xff0c;但是计算上不再需要TP,FP,FN等量了&#xff1a;&#xff09; 比如对A, B, C三类有如…

SpringBoot中如何集成ThymeLeaf呢?

转自: SpringBoot中如何集成ThymeLeaf呢&#xff1f; 下文笔者将讲述SpringBoot集成ThymeLeaf的方法&#xff0c;如下所示: 实现思路:1.在pom.xml中引入ThymeLeaf的相关依赖2.在Templates文件夹下编写相应的模板文件例: 1.pom.xml 添加ThymeLeaf依赖<!-- ThymeLeaf 依赖…

河南某商务楼BA系统设计

目 录 第一章 概述 3 第二章 设计任务与要求 4 第三章 设计依据和规范 4 第四章 系统设计 5 4.1系统选型 5 4.2 I/O点位设计 7 4.2.1暖通空调系统 11 4.2.2给排水系统 13 4.2.3电气系统 15 4.3线缆选型设计 17 4.4供电接地设计 17 4.5中央控制室设计 18 第五章 设备清单配置 18…

ASEMI代理力特LSIC1MO120E0080碳化硅MOSFET

编辑-Z 力特碳化硅MOS管LSIC1MO120E0080参数&#xff1a; 型号&#xff1a;LSIC1MO120E0080 漏极-源极电压&#xff08;VDS&#xff09;&#xff1a;1200V 连续漏电流&#xff08;ID&#xff09;&#xff1a;25A 功耗&#xff08;PD&#xff09;&#xff1a;214W 工作结温…

mysql数据库日志

1、日志类型 mysql日志在mysql事务章有事务日志相关的记录。初次之外&#xff0c;MySQL有不同类型的日志文件&#xff0c;用来存储不同类型的日志&#xff0c;分为二进制日志 、 错误日志 通用查询日志和 查询日志 &#xff0c;这也是常用的4种。MySQL 8又新增两种支持的日志&…

关于HTTPDNS,你知道多少?

导读&#xff1a; 全网域名劫持率高&#xff0c;域名解析失败、解析超时&#xff0c;IP调度不精准&#xff0c;域名解析变更生效不实时&#xff0c;这些问题是否一直困扰着你&#xff1f;作为网络请求最前置的环节&#xff0c;域名解析的稳定与精准程度直接决定了APP的访问体验…

实战讲解SpringCloud网关接口限流SpringCloudGateway+Redis(图+文)

1 缘起 最近补充微服务网关相关知识&#xff0c;学习了网关相关概念&#xff0c; 了解网关在微服务中存在的意义及其使命&#xff0c;如统一用户认证、接口权限控制、接口限流、接口熔断、黑白名单机制等&#xff0c; 打算通过实践的方式逐步学习网关的相关功能&#xff0c;同…