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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL DuckDB 分析主实例,集群系列 8核16GB
简介: SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战

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 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提交!这是一个都要重视的问题,必须要解决,不然可能会出现问题。

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

欢迎关注小编的微信公众号,文章首发看!谢谢支持哦!

1ce70671ddf0285e86d99990fc347a9.jpg

相关文章
|
2月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
234 5
|
3月前
|
存储 NoSQL 前端开发
Redis专题-实战篇一-基于Session和Redis实现登录业务
本项目基于SpringBoot实现黑马点评系统,涵盖Session与Redis两种登录方案。通过验证码登录、用户信息存储、拦截器校验等流程,解决集群环境下Session不共享问题,采用Redis替代Session实现数据共享与自动续期,提升系统可扩展性与安全性。
250 3
Redis专题-实战篇一-基于Session和Redis实现登录业务
|
3月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
201 1
Redis专题-实战篇二-商户查询缓存
|
9月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
900 0
分布式爬虫框架Scrapy-Redis实战指南
|
3月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
221 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
6月前
|
缓存 监控 NoSQL
Redis 实操要点:Java 最新技术栈的实战解析
本文介绍了基于Spring Boot 3、Redis 7和Lettuce客户端的Redis高级应用实践。内容包括:1)现代Java项目集成Redis的配置方法;2)使用Redisson实现分布式可重入锁与公平锁;3)缓存模式解决方案,包括布隆过滤器防穿透和随机过期时间防雪崩;4)Redis数据结构的高级应用,如HyperLogLog统计UV和GeoHash处理地理位置。文章提供了详细的代码示例,涵盖Redis在分布式系统中的核心应用场景,特别适合需要处理高并发、分布式锁等问题的开发场景。
415 41
|
6月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
1632 7
|
6月前
|
机器学习/深度学习 存储 NoSQL
基于 Flink + Redis 的实时特征工程实战:电商场景动态分桶计数实现
本文介绍了基于 Flink 与 Redis 构建的电商场景下实时特征工程解决方案,重点实现动态分桶计数等复杂特征计算。通过流处理引擎 Flink 实时加工用户行为数据,结合 Redis 高性能存储,满足推荐系统毫秒级特征更新需求。技术架构涵盖状态管理、窗口计算、Redis 数据模型设计及特征服务集成,有效提升模型预测效果与系统吞吐能力。
609 2
|
9月前
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
793 79
|
6月前
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
434 0