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

news2024/11/28 16:29:58

目录

    • 一、前言
    • 二、主流实现方案介绍
      • 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 + 自定义注解实现接口幂等性灵活性很高,只对需要进行幂等判断的接口加上注解即可,本文只是做了核心逻辑实现,对于实际项目中使用只要进行简单改造即可。

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

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

相关文章

部署项目时常用的 Linux 命令

目录 1 前言2 SSH登录命令3 SCP传输命令4 CP拷贝命令5 MV移动命令6 TAR解压命令7 DU查看文件夹/文件大小8 TAIL查看日志9 NOHUP后台运行10 结语 1 前言 在应用部署过程中&#xff0c;Linux命令是必不可少的工具。它们能够帮助我们管理文件、连接服务器、拷贝文件、查看日志以及…

大数据项目--学习笔记

新零售项目介绍 1&#xff0c;行业背景介绍 一&#xff0c;百货商店 百货商店是世界商业史上第一个实行新销售方法的现代大量销售组织。其新型销售方法有&#xff1a; 1&#xff0e;顾客可以毫无顾忌地、自由自在地进出商店&#xff1b; 2&#xff0e;商品销售实行“明码标价…

java基础语法总结

导言&#xff1a; Java语言是一种面向对象的编程语言&#xff0c;具有简单、可移植、安全、高性能等特点。本篇文章主要对java的基础的语法进行一个简单的总结和概述。 目录 导言&#xff1a; 正文&#xff1a; 1. 数据类型与变量 2. 运算符与逻辑控制 3. 方法 4. 数组…

数据结构 / 结构体字节计算

1. 结构体的存储 结构体各个成员的地址是连续的结构体变量的地址是第一个成员的地址 2. 64位操作系统8字节对齐 结构体的总字节大小是各个成员字节的总和&#xff0c;字节的总和需要是最宽成员的倍数结构体的首地址是最宽成员的倍数结构体各个成员的偏移量是该成员字节的倍数…

服务号和订阅号哪个好

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;在推送频率上来看&#xff0c;服务号每月能推送四条消息&#xff0c;而订阅号可以每天&#xff08;24小时&#xff09;推送一条消息。如果企业开通公众号的目的是提供服务&#xff0c;例如售前资讯…

JOSEF约瑟 BLD-20高压漏电保护继电器 50-1000ma AC220V

系列型号 BLD-20A高压漏电保护继电器 BLD-20高压漏电继电器 BLD-20高压漏电保护继电器 BLD-20X高压漏电保护装置 BLD-G20X高压漏电保护装置 用途 BLD-20高压漏电保护装置 (以下简称继电器)主用于交流电压1-10KV系统中,频率为50HZ,对供电系统的漏电(或接地)实现有选择性保…

MyBatis 操作数据库(入门)

一&#xff1a;MyBatis概念 (1)MyBatis &#x1f497;MyBatis是一款优秀的持久层框架&#xff0c;用于简化JDBC的开发 (2)持久层 1.持久层 &#x1f49c;持久层&#xff1a;持久化操作的层&#xff0c;通常指数据访问层(dao)&#xff0c;是用来操作数据库的 2.持久层的规范 ①…

【Web】Ctfshow Thinkphp3.2.3代码审计(1)

目录 ①web569 ②web570 ③web571 ④web572 ①web569 基础考察 /index.php/Admin/Login/ctfshowLogin ②web570 提示找路由 查看附件源码 (config.php) 发现定义了一个可执行命令的路由规则 /index.php/ctfshow/assert/eval($_POST[1]) 1system(tac /f*); ③web571 提…

The module to import is incompatible with the current project【鸿蒙开发-BUG已解决】

文章目录 项目场景:问题描述原因分析:解决方案:心得体会:知识点OpenHarmony:HarmonyOS:项目场景: 报错: The module to import is incompatible with the current project 问题描述 希望通过 import module 将该模块引入到我的项目。 导入后出现错误,因为项目和模块…

Scannet v2 数据集介绍以及子集下载展示

Scannet v2 数据集介绍以及子集下载展示 文章目录 Scannet v2 数据集介绍以及子集下载展示参考数据集简介子集scannet_frames_25kscannet_frames_test 下载脚本 download_scannetv2.py 参考 scannet数据集简介和下载-CSDN博客 scannet v2 数据集下载_scannetv2数据集_蓝羽飞鸟的…

pandas分组选中最大值并且新增列

题目 根据每个session_id分组&#xff0c;将popular最大的值设为这个session中所有popular的值 category item_id label popular session_id 0 4729 True 53.0 4069 0 4729 True 53.0 4069 0 4729 True 53.0 4069 0…

C++ PCL点云dscan密度分割三维

程序示例精选 C PCL点云dscan密度分割三维 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《C PCL点云dscan密度分割三维》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。…

hdlbits系列verilog解答(7420 chip)-49

文章目录 一、问题描述二、verilog源码三、仿真结果一、问题描述 本次将实现7420逻辑芯片,它内部有2个4输入的与非门电路,外部有8个输入和2个输出管脚,功能框图如下所示: 二、verilog源码 module top_module ( input p1a, p1b, p1c, p1d,output p1y,input p2a, p2b, p2c…

Proteus仿真--基于数码管显示的频率计设计

本文介绍基于数码管的频率计设计&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 本设计中80C51单片机作为主控&#xff0c;用数码管作为显示模块&#xff0c;按下按键K1后可进行频率测量并显示 仿真运行视频 Proteus仿真--数码管显示的频率计 附完整Pro…

如何使用nginx部署静态资源

Nginx可以作为静态web服务器来部署静态资源&#xff0c;这个静态资源是指在服务端真实存在&#xff0c;并且能够直接展示的一些文件数据&#xff0c;比如常见的静态资源有html页面、css文件、js文件、图片、视频、音频等资源相对于Tomcat服务器来说&#xff0c;Nginx处理静态资…

学习.NET验证模块FluentValidation的基本用法(续3:ASP.NET Core中的调用方式)

FluentValidation模块支持在ASP.NET Core项目中进行手工或自动验证&#xff0c;主要验证方式包括以下三种&#xff1a;   1&#xff09;手工注册验证类&#xff0c;并在控制器或其它模块中调用验证&#xff1b;   2&#xff09;基于ASP.NET验证管道&#xff08;validation …

【版本管理 | Git】Git rebase 命令最佳实践!确定不来看看?

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

webshell之内置函数免杀

原始webshell 查杀的点在于Runtime.getRuntime().exec非常明显的特征 利用ProcessBuilder替换Runtime.getRuntime().exec(cmd) Runtime.getRuntime().exec(cmd)其实最终调用的是ProcessBuilder这个函数&#xff0c;因此我们可以直接利用ProcessBuilder来替换Runtime.getRunti…

css优化滚动条样式

css代码&#xff1a; ::-webkit-scrollbar {width: 6px;height: 6px; }::-webkit-scrollbar-track {background-color: #f1f1f1; }::-webkit-scrollbar-thumb {background-color: #c0c0c0;border-radius: 3px; }最终样式&#xff1a;

大数据面试大厂真题【附答案详细解析】

1.Java基础篇&#xff08;阿里、蚂蚁、字节、携程、快手、杭州银行等&#xff09; 问题&#xff1a;HashMap的底层实现原理 答案&#xff1a; 在jdk1.8之前&#xff0c;hashmap由 数组-链表数据结构组成&#xff0c;在jdk1.8之后hashmap由 数组-链表-红黑树数据结构组成&…