Aop注解+Redis解决SpringBoot接口幂等性(源码自取)

目录

一、什么是幂等性?

二、哪些请求天生就是幂等的?

三、为什么需要幂等

1.超时重试

2.异步回调

3.消息队列

四、实现幂等的关键因素

关键因素1

关键因素2

五、引入幂等性后对系统的影响

六、Restful API 接口的幂等性

实战Aop注解+redis解决幂等性

reidis解决主要流程

添加所需依赖

application简单配置

封装自定义注解 

 封装统一请求入参对象

Header

Order

 ​编辑RequestData

定义Aop切面 

生成token

​编辑 启动测试

 简单总结

源码 


一、什么是幂等性?


简单来说,就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的,听起来非常容易理解,但要真正的在系统中要始终保持这个目标,是需要很严谨的设计的,在实际的生产环境下,我们应该保证任何接口都是幂等的

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

二、哪些请求天生就是幂等的?


首先,我们要知道查询类的请求一般都是天然幂等的,除此之外,删除请求在大多数情况下也是幂等的,但是ABA场景下除外。

举一个简单的例子
比如,先请求了一次删除A的操作,但由于响应超时,又自动请求了一次删除A的操作,如果在两次请求之间,又插入了一次A,而实际上新插入的这一次A,是不应该被删除的,这就是ABA问题,不过,在大多数业务场景中,ABA问题都是可以忽略的。

除了查询和删除之外,还有更新操作,同样的更新操作在大多数场景下也是天然幂等的,其例外是也会存在ABA的问题,更重要的是,比如执行update table set a = a + 1 where v = 1这样的更新就非幂等了。

最后,就还剩插入了,插入大多数情况下都是非幂等的,除非是利用数据库唯一索引来保证数据不会重复产生。

在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:

  • 前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。
  • 用户恶意进行刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。
  • 接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • 消息进行重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。

三、为什么需要幂等


1.超时重试


当发起一次RPC请求时,难免会因为网络不稳定而导致请求失败,一般遇到这样的问题我们希望能够重新请求一次,正常情况下没有问题,但有时请求实际上已经发出去了,只是在请求响应时网络异常或者超时,此时,请求方如果再重新发起一次请求,那被请求方就需要保证幂等了。

2.异步回调


异步回调是提升系统接口吞吐量的一种常用方式,很明显,此类接口一定是需要保证幂等性的。

3.消息队列


现在常用的消息队列框架,比如:Kafka、RocketMQ、RabbitMQ在消息传递时都会采取At least once原则(也就是至少一次原则,在消息传递时,不允许丢消息,但是允许有重复的消息),既然消息队列不保证不会出现重复的消息,那消费者自然要保证处理逻辑的幂等性了。

四、实现幂等的关键因素


关键因素1

幂等唯一标识,可以叫它幂等号或者幂等令牌或者全局ID,总之就是客户端与服务端一次请求时的唯一标识,一般情况下由客户端来生成,也可以让第三方来统一分配。

关键因素2

有了唯一标识以后,服务端只需要确保这个唯一标识只被使用一次即可,一种常见的方式就是利用数据库的唯一索引

五、引入幂等性后对系统的影响

幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:

  • 把并行执行的功能改为串行执行,降低了执行效率。
  • 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。

六、Restful API 接口的幂等性

现在流行的 Restful 推荐的几种 HTTP 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:

  • √ 满足幂等
  • x 不满足幂等
  • - 可能满足也可能不满足幂等,根据实际业务逻辑有关

实战Aop注解+redis解决幂等性

reidis解决主要流程

  • ① 服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串。
  • ② 客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
  • ③ 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
  • ④ 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
  • ⑤ 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
  • ⑥ 服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
  • ⑦ 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

注意,在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 表达式来注销查询与删除操作。

添加所需依赖

 <dependencies>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-core</artifactId>
            <version>1.8.5</version> <!-- 替换成你需要的版本 -->
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>
        <!--        web开发场景启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application简单配置

封装自定义注解 

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * 参数名,表示将从哪个参数中获取属性值。
     * 获取到的属性值将作为KEY。
     *
     * @return
     */
    String name() default "";

    /**
     * 属性,表示将获取哪个属性的值。
     *
     * @return
     */
    String field() default "";

    /**
     * 参数类型
     *
     * @return
     */
    Class type();

}

 封装统一请求入参对象

Order
 RequestData

定义Aop切面 

import com.example.redisidempotence.annotation.Idempotent;
import com.example.redisidempotence.compent.RedisIdempotentStorage;
import com.example.redisidempotence.vo.RequestData;
import org.aspectj.lang.ProceedingJoinPoint;
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.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;

@Aspect
@Component
public class IdempotentAspect {

    @Resource
    private RedisIdempotentStorage redisIdempotentStorage;

    @Pointcut("@annotation(com.example.redisidempotence.annotation.Idempotent)")
    public void idempotent() {
    }

    @Around("idempotent()")
    public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Idempotent idempotent = method.getAnnotation(Idempotent.class);

        String field = idempotent.field();
        String name = idempotent.name();
        Class clazzType = idempotent.type();

        String token = "";

        Object object = clazzType.newInstance();
        Map<String, Object> paramValue = AopUtils.getParamValue(joinPoint);
        if (object instanceof RequestData) {
            RequestData idempotentEntity = (RequestData) paramValue.get(name);
            token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));
        }
        if (redisIdempotentStorage.delete(token)) {
            return joinPoint.proceed();
        }
        if(token.isEmpty()){
            return "token校验失败,token不可为空";
        }
        return "不能重复请求";
    }
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class AopUtils {

    public static Object getFieldValue(Object obj, String name) throws Exception {
        Field[] fields = obj.getClass().getDeclaredFields();
        Object object = null;
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.getName().toUpperCase().equals(name.toUpperCase())) {
                object = field.get(obj);
                break;
            }
        }
        return object;
    }
    public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint) {
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> param = new HashMap<>(paramNames.length);

        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }
}

生成token

 定义redis服务接口及实现方法

 生成token接口

模拟业务请求controller

 启动测试

首先拿取到token

将拿取到的token当做header中token传入到模拟的业务请求中

 

可以看到,将拿取到的token放在模拟真实请求的业务中时,只有第一次请求返回成功,随后的请求都返回"不能重复请求",或者token传空时也返回了相应的token校验信息,从而实现了幂等性

 简单总结

幂等性是开发当中很常见也很重要的一个需求,尤其是支付、订单等与金钱挂钩的服务,保证接口幂等性尤其重要。在实际开发中,我们需要针对不同的业务场景我们需要灵活的选择幂等性的实现方式:

  • 对于下单等存在唯一主键的,可以使用“唯一主键方案”的方式实现。
  • 对于更新订单状态等相关的更新场景操作,使用“乐观锁方案”实现更为简单。
  • 对于上下游这种,下游请求上游,上游服务可以使用“下游传递唯一序列号方案”更为合理。
  • 类似于前端重复提交、重复下单、没有唯一ID号的场景,可以通过 Token 与 Redis 配合的“防重 Token 方案”实现更为快捷。

以下是各种方法解决幂等性的优劣比较

源码 

幂等性源码demo

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

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

相关文章

STM32基本定时功能

1、定时器就是计数器。 2、怎么计数&#xff1f; 3、我们需要有一恒定频率的方波信号&#xff0c;再加上一个寄存器。 4、比如每来一个上升沿信号&#xff0c;寄存器值加1&#xff0c;就可以完成计数。 5、假设方波频率是100Hz&#xff0c;也就是1秒100个脉冲。…

海外媒体宣发套餐如何利用3种方式洞察市场-华媒舍

在当今数字化时代&#xff0c;媒体宣发成为了企业推广产品和品牌的重要手段之一。其中&#xff0c;7FT媒体宣发套餐是一种常用而有效的宣传方式。本文将介绍这种媒体宣发套餐&#xff0c;以及如何利用它来洞察市场。 一、关键概念 在深入讨论7FT媒体宣发套餐之前&#xff0c;让…

Django工具

一、分页器介绍 1.1、介绍 分页,就是当我们在页面中显示一些信息列表,内容过多,一个页面显示不完,需要分成多个页面进行显示时,使用的技术就是分页技术 在django项目中,一般是使用3种分页的技术: 自定义分页功能,所有的分页功能都是自己实现django的插件 django-pagin…

机器学习笔记 大语言模型是如何运作的?一、语料库和N-gram模型

一、语料库 语言模型、ChatGPT和人工智能似乎无处不在。了解大型语言模型(LLM)“背后”发生的事情将是驾驭数字世界的关键。 首先在提示中键入一个单词,然后点击提交。您可以尝试新的提示,并根据需要多次重新生成响应。 这个我们称之为“T&C”的语言模型是在一…

大模型概念解析 | In-context Learning

注1:本文系"概念解析"系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:大模型中的In-context Learning 大模型概念解析 | In-context Learning PR-418: What learning algorithm is in-context learning? Investigations with linear mo…

【CSP试题回顾】202012-1-期末预测之安全指数

CSP-202012-1-期末预测之安全指数 解题代码 #include <iostream> #include <algorithm> using namespace std;int n, sum;int main() {cin >> n;for (int i 0; i < n; i){int w, s;cin >> w >> s;sum w * s;}sum max(sum, 0);cout <&…

RocketMQ快速入门_2. rocketmq 的应用场景、与其他mq的差异

0. 引言 之前我们讲解过rabbitMQ&#xff0c;本期我们将进入吞吐量更加强大的rocketMQ的学习。 1. 基础概念 如果你是刚接触MQ的同学&#xff0c;还不清楚消息队列的基础概念的&#xff0c;可以参考我之前这篇文章&#xff1a; https://wu55555.blog.csdn.net/article/deta…

JPEG照片被误删除如何恢复?学会这个方法就够了

JPG/JPEG是一种后缀名为“.jpg”或“.jpeg”的图形格式。它是存储照片图像的常用格式&#xff0c;因此我们可以使用数码相机、手机或其他设备来获取大量的JPG/JPEG文件。有时&#xff0c;我们会遇到由于意外删除、格式化驱动器或其他未知原因导致 JPEG 文件丢失的情况。无论哪种…

外包干了30天,技术明显退步。。

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 这次来聊一个大家可能也比较关心的问题&#xff0c;那就是就业城市选择的问题。而谈到这个问题&a…

程序员失业,被迫开启 PlanB——成为自由职业/独立开发者的第 0 天

程序员失业&#xff0c;被迫开启 PlanB——成为自由职业/独立开发者的第 0 天 今天在逛V2EX的时候看到的一个帖子&#xff0c;程序员中年被裁&#xff0c;被迫开启独立开发这条路。 原贴如下&#xff1a; lastday, 失业啦 公司年前通知我合同到期不续签&#xff0c;今天是我…

seq2seq翻译实战-Pytorch复现

&#x1f368; 本文为[&#x1f517;365天深度学习训练营学习记录博客 &#x1f366; 参考文章&#xff1a;365天深度学习训练营 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制]\n&#x1f680; 文章来源&#xff1a;[K同学的学习圈子](https://www.yuque.com/…

有点NB的免费wordpress主题模板

一个不错的黄色模板&#xff0c;用WP免费主题模板搭建家政服务公司网站。 https://www.wpniu.com/themes/15.html

Spring Boot中Excel数据导入导出的高效实现

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

数据结构之八大排序

&#x1d649;&#x1d65e;&#x1d658;&#x1d65a;!!&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦ &#x1f44f;&#x1f3fb;‧✧̣̥̇:Solitary_walk ⸝⋆ ━━━┓ - 个性标签 - &#xff1a;来于“云”的“羽球人”。…

若依/RuoYi-Vue使用docker-compose部署

系统需求 JDK > 1.8 MySQL > 5.7 Maven > 3.0 Node > 12 Redis > 3 思路 前端服务器 nginx 后端服务器代码打包 java、maven、node 数据库/缓存 mysql、redis 开始 创建目录ruoyi并进入 克隆若依代码 git clone RuoYi-Vue: &#x1f389; 基于Spring…

【Kotlin】类和对象

1 前言 Kotlin 是面向对象编程语言&#xff0c;与 Java 语言类似&#xff0c;都有类、对象、属性、构造函数、成员函数&#xff0c;都有封装、继承、多态三大特性&#xff0c;不同点如下。 Java 有静态&#xff08;static&#xff09;代码块&#xff0c;Kotlin 没有&#xff1…

探索 ON1 Resize Ai 2023.5 for Mac/win:释放图像的无限可能

ON1 Resize AI 2023.5 for Mac/Win 是一款专业的图像无损放大软件&#xff0c;通过人工智能技术&#xff0c;能够将图像放大至更高的分辨率&#xff0c;同时保持图像细节和清晰度的最佳状态。该软件的强大功能和直观的操作界面&#xff0c;使它成为摄影师、设计师和艺术家的理想…

WinCE USB驱动架构及术语明析

一、层式驱动的概念。 WinCE驱动多为层式驱动&#xff0c;分为MDD和PDD两层。 MDD包含通用的驱动代码&#xff0c;向操作系统提供了驱动接口&#xff0c;该层代码调用PDD功能访问硬件。 PDD部分包含与硬件平台相关的特殊代码&#xff0c;不具有通用性。 之所以要分层&#xff0…

day-18 轮转数组

时间复杂度为O&#xff08;n&#xff09; code: class Solution {public void rotate(int[] nums, int k) {int nnums.length;kk%n;int arr[]new int[n];for(int i0;i<n;i){arr[(ik)%n]nums[i];}for(int i0;i<n;i){nums[i]arr[i];}} }参考答案 进行三次翻转 空间复杂度O…

03_Tomcat

文章目录 Tomcat概念自制简易的服务器JavaEE规范Tomcat安装Tomcat启动Tomcat的资源部署直接部署虚拟映射 Tomcat的设置 Tomcat 概念 服务器&#xff1a;两层含义。 软件层面&#xff1a;软件&#xff0c;可以将本地的资源发布到网络中&#xff0c;供网络上面的其他用户来访问…