SpringBoot 环境使用 Redis + AOP + 自定义注解实现接口幂等性

目录

    • 一、前言
    • 二、主流实现方案介绍
      • 2.1、前端按钮做加载状态限制(必备)
      • 2.2、客户端使用唯一标识符
      • 2.3、服务端通过检测请求参数进行幂等校验(本文使用)
    • 三、代码实现
      • 3.1、POM
      • 3.2、application.yml
      • 3.3、Redis配置类
      • 3.4、自定义注解
      • 3.5、AOP切面实现
      • 3.6、接口测试幂等效果
    • 四、总结

一、前言

      接口幂等性是指在相同的条件下,对一个接口的多次调用所产生的效果与单次调用的效果相同。简而言之,无论调用一个接口多少次,系统的状态都应该保持一致,不会因为多次调用而产生不同的结果。
      在Web开发中,特别是在RESTful API设计中,幂等性是一个重要的概念。具有幂等性的接口在面对网络不稳定、消息重复发送或者其他异常情况时更容易处理,因为它们能够保证多次相同的请求不会导致意外的副作用。

二、主流实现方案介绍

2.1、前端按钮做加载状态限制(必备)

      对于前端而言在处理下单提交按钮时一定要加上加载状态,在调用接口时如果还没有响应不允许再次点击,如果服务端没做幂等判断那么用户快速点击多次提交按钮就可能产生多比一样的订单,而且就算服务端做了幂等判断这样可以快速点击调用多次下单接口的操作也是有问题的。

2.2、客户端使用唯一标识符

      在每次发送请求时,客户端生成一个唯一的请求RequestId并将这个RequestId放在请求的头部或参数中。服务器端在接收到请求时,先验证RequestId是否已经被使用过。如果已经被使用过,说明请求重复,直接返回结果。否则,处理请求并标记该RequestId为已使用。

2.3、服务端通过检测请求参数进行幂等校验(本文使用)

      这种方式不需要前端配合,具体实现方式是获取到接口的请求参数,对请求参数进行hash或者md5,然后将这个hash之后的请求参数作为key存储在Redis中并且设置一个过期时间,每次请求时都会先判断缓存中是否存在这个key,如果存在则代表是重复提交,这种方式也是用的最多的,会结合AOP+自定义注解实现,使用分页非常灵活。

三、代码实现

3.1、POM

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--springboot中的redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- lettuce pool 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

3.2、application.yml

server:
  port: 8000

spring:
  #redis配置信息
  redis:
    ## Redis数据库索引(默认为0)
    database: 0
    ## Redis服务器地址
    host: 127.0.0.1
    ## Redis服务器连接端口
    port: 6379
    ## Redis服务器连接密码(默认为空)
    password: '123456'
    ## 连接超时时间(毫秒)
    timeout: 5000
    lettuce:
      pool:
        ## 连接池最大连接数(使用负值表示没有限制)
        max-active: 10
        ## 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        ## 连接池中的最大空闲连接
        max-idle: 10
        ## 连接池中的最小空闲连接
        min-idle: 1

3.3、Redis配置类

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig{
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();
        return template;
    }
}

3.4、自定义注解

import java.lang.annotation.*;

/**
 * 自定义注解防止表单重复提交
 */
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmitCheck {

    /**
     * 业务标识,不传默认ALL,便于区分业务
     */
    String key() default "ALL";

    /**
     * 防重复提交保持时间,默认1s
     */
    int keepSeconds() default 1;
}

3.5、AOP切面实现

import com.redisscene.annotation.RepeatSubmitCheck;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private HttpServletRequest request;

    // 重复提交锁key
    private String RP_LOCK_RESTS = "RP_LOCK_RESTS:";

    @Pointcut("@annotation(com.redisscene.annotation.RepeatSubmitCheck)")
    public void requestPointcut() {
    }

    @Around("requestPointcut() && @annotation(repeatSubmitCheck)")
    public Object interceptor(ProceedingJoinPoint pjp, RepeatSubmitCheck repeatSubmitCheck) throws Throwable {
        final String lockKey = RP_LOCK_RESTS + repeatSubmitCheck.key() + ":" + generateKey(pjp);
        // 上锁 类似setnx,并且是原子性的设置过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "0", repeatSubmitCheck.keepSeconds(), TimeUnit.SECONDS);
        if (!lock) {
            // 这里也可以改为自己项目自定义的异常抛出 也可以直接return
//            throw new RuntimeException("重复提交");
            return "time="+ LocalDateTime.now() + " 重复提交";
        }
        return pjp.proceed();
    }

    private String generateKey(ProceedingJoinPoint pjp) {
        StringBuilder sb = new StringBuilder();
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        sb.append(pjp.getTarget().getClass().getName())//类名
                .append(method.getName());//方法名
        for (Object o : pjp.getArgs()) {
            if (o != null) {
                sb.append(o.toString());//参数
            }
        }
        String token = request.getHeader("token") == null ? "" : request.getHeader("token");
        sb.append(token);//token
        log.info("RP_LOCK generateKey() called with parameters => 【sb = {}】", sb);
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes(Charset.defaultCharset()));
    }
}

3.6、接口测试幂等效果

import com.redisscene.annotation.RepeatSubmitCheck;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class RepeatSubmitTestControlller {

    // curl -X GET -H "token: A001" "http://127.0.0.1:8000/t1?param1=nice&param2=hello"
    @RepeatSubmitCheck
    @GetMapping("/t1")
    public String t1(String param1,String param2){
        log.info("t1 param1={} param2={}",param1,param2);

        return "time="+LocalDateTime.now() + " t1";
    }
    // curl -X POST -H "token: A001" -H "Content-Type: application/json" -d "{'name':'kerwin'}" "http://127.0.0.1:8000/t2"
    @RepeatSubmitCheck(key = "T2",keepSeconds = 5)
    @PostMapping("/t2")
    public String t2(@RequestBody String body){
        log.info("t2 body={}",body);
        return "time="+LocalDateTime.now() + " t2";
    }
}

这里提供两个测试接口,我这里会使用curl进行测试可以直接在cmd命令行执行,也可以自己使用postman等工具测试。

  • t1 方法测试

    curl -X GET -H "token: A001" "http://127.0.0.1:8000/t1?param1=nice&param2=hello"
    

    在这里插入图片描述
    这里可以看到两次调用t1接口时如果在1s内再次调用会出现重复提交,过了1s后可以再次调用成功。

  • t2 方法测试

    curl -X POST -H "token: A001" -H "Content-Type: application/json" -d "{'name':'kerwin'}" "http://127.0.0.1:8000/t2"
    

    在这里插入图片描述
    这里可以看到两次调用t2接口时如果在5s内再次调用会出现重复提交,过了5s后可以再次调用成功。

四、总结

      通过Redis + AOP + 自定义注解实现接口幂等性灵活性很高,只对需要进行幂等判断的接口加上注解即可,本文只是做了核心逻辑实现,对于实际项目中使用只要进行简单改造即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Haclon的标签旋转项目案例

项目要求&#xff1a; 图为HALCON附图“25interleaved_exposure_04”&#xff0c;里面为旋转的二维码标签&#xff0c;请将其旋转到水平位置。 项目知识&#xff1a; 在HALCON中进行图像平移和旋转通常有以下步骤&#xff1a; &#xff08;1&#xff09;通过hom_mat2d_ident…

jQuery_03 dom对象和jQuery对象的互相转换

dom对象和jQuery对象 dom对象 jQuery对象 在一个文件中同时存在两种对象 dom对象: 通过js中的document对象获取的对象 或者创建的对象 jQuery对象: 通过jQuery中的函数获取的对象。 为什么使用dom或jQuery对象呢&#xff1f; 目的是 要使用dom对象的函数或者属性 以及呢 要…

<JavaEE> 线程的五种创建方法 和 查看线程的两种方式

目录 一、线程的创建方法 1.1 继承 Thread -> 重写 run 方法 1.2 使用匿名内部类 -> 继承 Thread -> 重写 run 方法 1.3 实现 Runnable 接口 -> 重写 run 方法 1.4 使用匿名内部类 -> 实现 Runnable 接口 -> 重写 run 方法 1.5 使用 lambda 表达式 二…

Self Distillation 自蒸馏论文解读

paper&#xff1a;Be Your Own Teacher: Improve the Performance of Convolutional Neural Networks via Self Distillation official implementation&#xff1a; https://github.com/luanyunteng/pytorch-be-your-own-teacher 前言 知识蒸馏作为一种流行的压缩方法&#…

五种多目标优化算法(MOGWO、MOLPB、MOJS、NSGA3、MOPSO)求解微电网多目标优化调度(MATLAB代码)

一、多目标优化算法简介 &#xff08;1&#xff09;多目标灰狼优化算法MOGWO 多目标应用&#xff1a;基于多目标灰狼优化算法MOGWO求解微电网多目标优化调度&#xff08;MATLAB代码&#xff09;-CSDN博客 &#xff08;2&#xff09;多目标学习者行为优化算法MOLPB 多目标学习…

ps5ps4游戏室如何计时?计费系统怎么查看游戏时间以及收费如何管理

ps5ps4游戏室如何计时&#xff1f;计费系统怎么查看游戏时间以及收费如何管理 1、ps5ps4游戏室如何计时&#xff1f; 下图以佳易王计时计费软件V17.9为例说明 在开始计时的时候&#xff0c;只需点 开始计时按钮&#xff0c;那么开台时间和使用的时间长度项目显示在屏幕上&am…

如何判断一个题目用“贪心/动态规划“还是用“BFS/DFS”方法解决

1 总结 1.1 贪心、动态规划和BFS/DFS题解的关系 一般能使用贪心、动态规划解决一个问题时&#xff0c;使用BFS&#xff0c;DFS也能解决这个题&#xff0c;但是反之不能成立。 1.2 2 贪心 -> BFS/DFS 2.1 跳跃游戏1和3的异同 这两道题&#xff0c;“跳跃游戏”&#xf…

靡靡之音 天籁之声 ——Adobe Audition

上一期讲到了和Pr配合使用的字幕插件Arctime Pro的相关介绍。相信还记得的小伙伴应该记得我还提到过一个软件叫做Au。 当人们对字幕需求的逐渐满足&#xff0c;我们便开始追求更高层次的享受&#xff0c;当视觉享受在进步&#xff0c;听觉享受想必也不能被落下&#xff01; Au即…

Flutter桌面应用开发之毛玻璃效果

目录 效果实现方案依赖库支持平台实现步骤注意事项话题扩展 毛玻璃效果&#xff1a;毛玻璃效果是一种模糊化的视觉效果&#xff0c;常用于图像处理和界面设计中。它可以通过在图像或界面元素上应用高斯模糊来实现。使用毛玻璃效果可以增加图像或界面元素的柔和感&#xff0c;同…

一、深入简出串口(USRT)通信——基本概念。

一、前言 串口到底是什么&#xff1f;简单来说一句话就可以解释&#xff0c;串口就是一种通信协议。 看到这里可能大家会觉得你这不是放屁么&#xff0c;说了跟没说一样。所以这里做前言来描述&#xff0c;大家要先对通信协议有一个下意识地认识才能在学习串口的时候不至于迷茫…

spring循环依赖

Bean的生命周期 这里不会对Bean的生命周期进行详细的描述&#xff0c;只描述一下大概的过程。 Bean的生命周期指的就是&#xff1a;在Spring中&#xff0c;Bean是如何生成的&#xff1f; 被Spring管理的对象叫做Bean。Bean的生成步骤如下&#xff1a; Spring扫描class得到Bean…

yolo系列中的一些评价指标说明

文章目录 一. 混淆矩阵二. 准确度(Accuracy)三. 精确度(Precision)四. 召回率(Recall)五. F1-score六. P-R曲线七. AP八. mAP九. mAP0.5十. mAP[0.5:0.95] 一. 混淆矩阵 TP (True positives)&#xff1a;被正确地划分为正例的个数&#xff0c;即实际为正例且被分类器划分为正例…

计算机编程基础教程,中文编程工具下载,编程构件组合按钮

计算机编程基础教程&#xff0c;中文编程工具下载&#xff0c;编程构件组合按钮 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&#xff0c;而且可以开发大型的软件&#xff0c…

人力资源管理后台 === 登陆+主页灵鉴权

目录 1. 分析登录流程 2. Vuex中用户模块的实现 3.Vue-cli代理解决跨域 4.axios封装 5.环境区分 6. 登录联调 7.主页权限验证-鉴权 1. 分析登录流程 传统思路都是登录校验通过之后&#xff0c;直接调用接口&#xff0c;获取token之后&#xff0c;跳转到主页。 vue-elemen…

C++二分查找:统计点对的数目

本题其它解法 C双指针算法&#xff1a;统计点对的数目 本周推荐阅读 C二分算法&#xff1a;得到子序列的最少操作次数 本文涉及的基础知识点 二分查找算法合集 题目 给你一个无向图&#xff0c;无向图由整数 n &#xff0c;表示图中节点的数目&#xff0c;和 edges 组成…

HTTP状态码:如何修复 404 Not Found错误?

互联网上各种类型的网站非常多&#xff0c;无论用户还是网站运营者不可避免的会遇到404 Not Found错误&#xff0c;如果遇到404错误&#xff0c;我们应该如何解决呢&#xff1f; 对于用户 检查拼写错误 如果您是遇到错误的用户&#xff0c;请仔细检查 URL 是否有任何拼写错误…

【Flutter 常见问题系列 第 1 篇】Text组件 文字的对齐、数字和字母对齐中文

TextStyle中设置height参数即可 对齐的效果 Text的高度 是根据 height 乘于 fontSize 进行计算的、这里指定heiht即可、不指定的会出现 无法对齐的情况&#xff0c;如下&#xff1a; 这种就是无法对齐的情况

决策树(第四周)

一、决策树基本原理 如下图所示&#xff0c;是一个用来辨别是否是猫的二分类器。输入值有三个&#xff08;x1&#xff0c;x2&#xff0c;x3&#xff09;&#xff08;耳朵形状&#xff0c;脸形状&#xff0c;胡须&#xff09;&#xff0c;其中x1{尖的&#xff0c;圆的}&#xf…

R语言实现Lasso回归

一、Lasso回归 Lasso 回归&#xff08;Least Absolute Shrinkage and Selection Operator Regression&#xff09;是一种用于线性回归和特征选择的统计方法。它在回归问题中加入了L1正则化项&#xff0c;有助于解决多重共线性&#xff08;多个特征高度相关&#xff09;和特征选…

什么是轻量应用服务器?可以从亚马逊云科技的优势入手了解

什么是轻量应用服务器&#xff1f; 随着如今各行各业对云计算的需求越来越多&#xff0c;云服务器也被越来越多的企业所广泛采用。其中&#xff0c;轻量应用服务器是一种简单、高效、可靠的云计算服务&#xff0c;能够为开发人员、企业和个人提供轻量级的虚拟专用服务器&#x…