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: 8000spring:#redis配置信息redis:## Redis数据库索引(默认为0)database: 0## Redis服务器地址host: 127.0.0.1## Redis服务器连接端口port: 6379## Redis服务器连接密码(默认为空)password: '123456'## 连接超时时间(毫秒)timeout: 5000lettuce: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{@Beanpublic 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和publicom.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 {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate HttpServletRequest request;// 重复提交锁keyprivate 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);//tokenlog.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¶m2=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¶m2=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 + 自定义注解实现接口幂等性灵活性很高,只对需要进行幂等判断的接口加上注解即可,本文只是做了核心逻辑实现,对于实际项目中使用只要进行简单改造即可。
相关文章:
SpringBoot 环境使用 Redis + AOP + 自定义注解实现接口幂等性
目录 一、前言二、主流实现方案介绍2.1、前端按钮做加载状态限制(必备)2.2、客户端使用唯一标识符2.3、服务端通过检测请求参数进行幂等校验(本文使用) 三、代码实现3.1、POM3.2、application.yml3.3、Redis配置类3.4、自定义注解…...
Leetcode—18.四数之和【中等】
2023每日刷题(四十一) Leetcode—18.四数之和 实现代码 class Solution { public:vector<vector<int>> fourSum(vector<int>& nums, int target) {vector<vector<int>> ans;sort(nums.begin(), nums.end());int n …...
springsecurity6配置二
一、springsecurity6自定义认证异常处理器 1.1 AuthenticationEntryPointImpl.java package com.school.information.core.security.handler;import com.alibaba.fastjson.JSON; import com.school.information.enums.result.ResultStatusEnum; import com.school.informatio…...
php如何对比浮点数大小(bccomp函数)
第一部分,常规例子: 例1:左边比右边小,结果:-1 //示例,左边比右边小返回值:-1 $price1 2.14; $price2 3.14; $result bccomp($price1, $price2, 2); echo 对比结果:.$result;//…...
服务号和订阅号哪个好
服务号和订阅号有什么区别?服务号转为订阅号有哪些作用?在推送频率上来看,服务号每月能推送四条消息,而订阅号可以每天(24小时)推送一条消息。如果企业开通公众号的目的是提供服务,例如售前资讯…...
面试问题--智能指针
什么是智能指针? 当你在编写程序时,可能需要在运行时动态分配内存来存储数据。在传统的C中,你可能会使用 new 和 delete 操作符来手动管理内存。但是这样容易出现一些问题,比如忘记释放内存导致内存泄漏,或者释放了之…...
向量机SVM原理理解和实战
目录 概念场景导入 点到超平面的距离公式 最大间隔的优化模型 硬间隔、软间隔和非线性 SVM 用 SVM 如何解决多分类问题 1. 一对多法 2. 一对一法 SVM主要原理和特点 原理 优点 缺点 支持向量机模型分类 SVM实战如何进行乳腺癌检测 数据集 字段含义 代码实现 参…...
什么是 Node.js?
在 Node.js 出现之前,最常见的 JavaScript 运行时环境是浏览器,也叫做 JavaScript 的宿主环境。浏览器为 JavaScript 提供了 DOM API,能够让 JavaScript 操作浏览器环境(JS 环境)。 2009 年初 Node.js 出现了…...
系列九、声明式事务(xml方式)
一、概述 声明式事务(declarative transaction management)是Spring提供的对程序事务管理的一种方式,Spring的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,是指在配置文件中声明,用在Spring配置文件中声明式的处理事务来…...
c盘清理——常用方法和工具整理
背景 最近c盘满了,只剩下1-2G,周末有空清理一下。对这块不太熟悉,下面只是把今天网上看到的比较好用的工具整理一下。 使用工具 磁盘大小查看工具——TreeSize(收费) 之前都是右键一个个看每个文件的大小࿰…...
【React】打包体积分析 source-map-explorer
通过分析打包体积,才能知道项目中的哪部分内容体积过大,方便知道哪些包需要进一步优化。 使用步骤 安装分析打包体积的包:npm i source-map-explorer在 package.json 中的 scripts 标签中,添加分析打包体积的命令对项目打包&…...
Zookeeper(一):在WSL单机搭建Zookeeper伪集群
目录 Zookeeper1 启动单个Zookeeper实例1.1 下载Zookeeper安装包并解压1.2 添加环境变量1.3 修改默认配置1.4 新建数据存储目录和日志目录1.5 启动Zookeeper1.6 停止Zookeeper 2 搭建Zookeeper集群2.1 新建集群目录2.2 配置环境变量2.3 创建节点目录2.4 修改配置2.5 创建节点ID…...
Go语法的特殊之处
上文我们讲了GO模块引入指令Go Mod,本文讲述Go语法的特殊之处 : 单变量 : hello:“hello” Go 语言中新增了一个特殊的运算符:,这个运算符可以使变量在不声明的情况下直接被赋值使用。其使用方法和带值声明变量类似,只是少了var关键字&…...
浏览器v8垃圾回收机制和内存泄漏分析-初级
借鉴:一文搞懂V8引擎的垃圾回收 - 掘金 (juejin.cn) 聊聊V8引擎的垃圾回收 - 掘金 (juejin.cn) 内存泄漏方向: 1、全局变量 未手动清除 2、定时器 未手动清除 3、闭包中使用了匿名函数 未手动清除 4、dom被赋值使用后 未手动清除 其他解决方式 1、…...
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…...
Sentinel核心类解读:Entry
默认情况下,Sentinel会将controller中的方法作为被保护资源,Sentinel中的资源用Entry来表示。 Sentinel中Entry可以理解为每次进入资源的一个凭证,如果调用SphO.entry()或者SphU.entry()能获取Entry对象,代表获取了凭证ÿ…...
YOLOv8改进 | SAConv可切换空洞卷积(附修改后的C2f+Bottleneck)
论文地址:官方论文地址 代码地址:官方代码地址 一、本文介绍 本文给大家带来的改进机制是可切换的空洞卷积(Switchable Atrous Convolution, SAC)是一种创新的卷积网络机制,专为增强物体检测和分割任务中的特征提取而…...
可以ping通IP但是无法远程连接-‘telnet‘ 不是内部或外部命令,也不是可运行的程序或批处理文件
起因 一开始远程连接IP,报错,怀疑是自己网络原因,但是同事依旧无法连接 怀疑是自己防火墙的原因,查看关闭依旧无法连接 问题 两个地址可以ping通排除防火墙缘故 怀疑端口,测试端口 然 解决方案 winR 输入control…...
使用VC++设计程序:实现常见的三种图像插值算法:最近邻插值,双线性插值,立方卷积插值
图像放大的三种插值算法 获取源工程可访问gitee可在此工程的基础上进行学习。 该工程的其他文章: 01- 一元熵值、二维熵值 02- 图像平移变换,图像缩放、图像裁剪、图像对角线镜像以及图像的旋转 03-邻域平均平滑算法、中值滤波算法、K近邻均值滤波器 04-…...
多级嵌套vue同步调用用法
//需求 要求同步调用initGame2方法 //调用方法 this.initSocket(); //定义方法为同步 async initSocket() { //调用为同步 await this.initGame2(); //定义方法为同步 async initGame2() {const e await w({ url: //定义w方法 const w e.create({ baseURL: http://my_url:8…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)
升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点,但无自动故障转移能力,Master宕机后需人工切换,期间消息可能无法读取。Slave仅存储数据,无法主动升级为Master响应请求ÿ…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...
Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...
