接口幂等实现基于注解(适用于分布式系统中支持spEl动态过滤)

news2024/11/20 22:38:47
  • 在网络卡顿时,容易出现在极短的时间内产生重复请求,或重复支付,一般我们会在数据操作时先通过数据查询是否存在,然后再进行业务逻辑操作的方式来进行避免,但是这种方式并不是原子性,很容易出现第一次请求未进行落表,第二次重复的请求就已经通过了数据库查询,可通过设置唯一索引的方式来避免,但是这样比较耗费性能的,如何把这种极端情况下的请求进行排除掉呢

  • 设计流程如下
    在这里插入图片描述

  • 代码实现

    • AOP拦截时需要支持spel表达式进行处理,因为很多的幂等请求都是动态的,本文中将基于Redisson进行接口幂等的实现
  • 首先创建一个注解spElValue是一个字符串数组,可以支持多种方式的组合来进行过滤

    package com.sw.xyz.springframework.cache.annotations;
    
    import java.lang.annotation.*;
    
    /**
     * 名称: IdemPoeNce
     * 功能: <功能详细描述>
     * 方法: <方法简述-方法描述>
     * 版本: 1.0
     * 作者: sunyw
     * 说明: 幂等性注解,使用SpEl表达式进行解析,目的是为了解决同一时间节点的重复请求
     * 时间: 2022/12/21 0021 17:58
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface IdemPoeNce {
    
        /**
         * 解析的数据值
         */
        String[] spElValue() default "";
    
        /**
         * 时间,时间范围,时间秒
         */
        long time() default 1;
    }
    
    
  • 对应的切面,逻辑为,请求时从对象中取spEl表达式对应的value,拼装类的全路径和方法名称+参数值作为幂等的条件值,切面中的异常处理和返回参数可以按照实际的情况做处理,文本做的方式是直接返回错误提示信息

    package com.sw.xyz.springframework.cache.config;
    
    import cn.hutool.core.lang.Validator;
    import com.sw.xyz.springframework.bean.entity.enums.RespCodeEnums;
    import com.sw.xyz.springframework.bean.exceptions.BaseException;
    import com.sw.xyz.springframework.cache.annotations.IdemPoeNce;
    import com.sw.xyz.springframework.cache.redisson.RedissonBaseUtils;
    import com.sw.xyz.springframework.utils.spel.SpElUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 名称: IdemPoeNceAspect
     * 功能: <幂等性校验注解>
     * 方法: <方法简述-方法描述>
     * 版本: 1.0
     * 作者: sunyw
     * 说明: 在一段时间节点内防止重复点击
     * 时间: 2022/12/21 0021 18:01
     */
    @Aspect
    @Component
    @Slf4j
    public class IdemPoeNceAspect {
    
        @Autowired
        private RedissonBaseUtils<String> baseUtils;
    
        @Autowired
        private SpElUtils spElUtils;
    
    
        @Pointcut(value = "@annotation(com.sw.xyz.springframework.cache.annotations.IdemPoeNce)")
        public void cut() {
        }
    
        @Before(value = "cut()")
        public void check(JoinPoint joinPoint) {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            IdemPoeNce poeNce = method.getAnnotation(IdemPoeNce.class);
            if (null == poeNce) {
                return;
            }
            String value = buildKey(poeNce, method, joinPoint.getArgs());
            boolean result = baseUtils.TrySaveObject(value, value, poeNce.time(), TimeUnit.SECONDS);
            if (!result) {
                throw new BaseException(RespCodeEnums.REPEATABLE_REQUESTS.getCode(), RespCodeEnums.REPEATABLE_REQUESTS.getMessage());
            }
        }
    
        /**
         * 构建幂等性的Key,由Key值加动态参数值构成
         *
         * @param rateLimit {@link RateLimit}
         * @param method    {@link Method}
         * @return String
         */
        private String buildKey(IdemPoeNce rateLimit, Method method, Object[] args) {
            String value = method.getDeclaringClass().getName() + "." + method.getName();
            String spElValue = spElUtils.parseSpEl(method, rateLimit.spElValue(), args);
            if (Validator.isNotEmpty(spElValue)) {
                value = value + "#" + spElValue;
            }
            return value;
        }
    }
    
    
  • Redisson工具类

    package com.sw.xyz.springframework.cache.redisson;
    
    import lombok.extern.slf4j.Slf4j;
    import org.redisson.api.RAtomicLong;
    import org.redisson.api.RBucket;
    import org.redisson.api.RMapCache;
    import org.redisson.api.RedissonClient;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 名称: Redisson 操作 Redis
     * 功能: 获取操作Redis的数据对象
     * Redisson 获取连接的数据对象, 可直接对Redis数据进行操作
     * 方法: <方法简述-方法描述>
     * 版本: 1.0
     * 作者: sunyw
     * 说明: 说明描述
     * 时间: 2021/12/7 10:05
     */
    @Component
    @Slf4j
    public class RedissonBaseUtils<T> {
    
        private final RedissonClient redissonClient;
    
        public RedissonBaseUtils(RedissonClient redissonClient) {
            this.redissonClient=redissonClient;
        }
        
        /**
         * 尝试保存Object,当已经存在一个值时,返回false
         *
         * @param key      键
         * @param value    值
         * @param time     过期时间
         * @param timeUnit 过期时间单位
         * @return true success
         */
        public boolean TrySaveObject(String key,T value,Long time,TimeUnit timeUnit) {
            try{
                RBucket<Object> bucket=redissonClient.getBucket(key);
                return bucket.trySet(value,time,timeUnit);
            } catch (Exception e) {
                log.error("TrySaveObject",e);
            }
            return false;
        }
    }
    
    
  • SpEl解析的工具类

    package com.sw.xyz.springframework.utils.spel;
    
    import cn.hutool.core.lang.Validator;
    import cn.hutool.core.util.ArrayUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.context.expression.BeanFactoryResolver;
    import org.springframework.context.expression.MethodBasedEvaluationContext;
    import org.springframework.core.DefaultParameterNameDiscoverer;
    import org.springframework.expression.BeanResolver;
    import org.springframework.expression.Expression;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.expression.spel.support.StandardEvaluationContext;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * 名称: SpelUtils
     * 功能: <功能详细描述>
     * 方法: <方法简述-方法描述>
     * 版本: 1.0
     * 作者: sunyw
     * 说明: 说明描述
     * 时间: 2022/12/2 0002 14:59
     */
    @Component
    @Slf4j
    public class SpElUtils {
        /**
         * 用于SpEL表达式解析.
         */
        private final static SpelExpressionParser parser = new SpelExpressionParser();
        /**
         * 用于获取方法参数定义名字.
         */
        private final static DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
    
        private BeanResolver beanResolver;
    
        public SpElUtils(BeanFactory beanFactory) {
            this.beanResolver = new BeanFactoryResolver(beanFactory);
        }
    
        public String parseSpEl(Method method, String[] keys, Object[] args) {
            StringBuilder sbu = new StringBuilder();
            try {
                if (!ArrayUtil.isEmpty(keys)) {
                    StandardEvaluationContext context = new MethodBasedEvaluationContext(null, method, args, nameDiscoverer);
                    context.setBeanResolver(beanResolver);
                    for (int i = 0; i < keys.length; i++) {
                        String value = keys[i];
                        if (Validator.isNotEmpty(value)) {
                            String parseValue = parser.parseExpression(value).getValue(context, String.class);
                            sbu.append(parseValue);
                            if (i < keys.length - 1) {
                                sbu.append(".");
                            }
                        }
                    }
                }
            } catch (Exception e) {
                log.error("spEl解析失败,错误信息[{}]", e.getMessage());
            }
            return sbu.toString();
        }
    }
    
    
  • 实际使用,只需要在接口上添加注解,spElValue指定请求对象的参数值
    在这里插入图片描述

  • 测试:使用相同的userId在一秒内重复请求
    在这里插入图片描述

  • 正常返回
    在这里插入图片描述

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

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

相关文章

用于开发语音 AI 应用程序的 GPU 加速 SDK

NVIDIA Riva 简介&#xff1a;用于开发语音 AI 应用程序的 GPU 加速 SDK 语音 AI 用于多种应用&#xff0c;包括联络中心的座席助理以增强人类座席的能力、智能虚拟助理 (IVA) 的语音界面以及视频会议中的实时字幕。 为了支持这些功能&#xff0c;语音 AI 技术包括自动语音识别…

基于springboot的社区团购管理系统的设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

FineReport企业报表工具-JS根据条件显示参数控件

1. 概述 1.1 版本 报表服务器版本 功能变更 11.0 -- 1.2 问题描述 在使用参数控件时&#xff0c;有时我们希望部分参数控件在没满足条件时不显示&#xff0c;满足条件后再显示&#xff0c;如下图效果&#xff0c;只有前面的下拉框选择了内容之后&#xff0c;后一层下拉框控…

【C语言】重要函数qsort函数的用法

目录 一、qsort函数的介绍 1、整形数组 2、字符数组 3、字符串 4、结构体 二、qsort函数的使用 一、qsort函数的介绍 qsort函数是一种底层快速排序的函数&#xff0c;它的特点就是可以排序任意类型的数据&#xff0c;比如&#xff1a;整形、字符串、浮点型、以及结构体类型。 …

VUE3-组件之间传值《四》

目录 一.父传子&#xff0c;父组件向子组件传值 二.子传父&#xff0c;子组件向父组件传值 三.兄弟组件之间互传&#xff0c;2个组件之间是平级关系&#xff0c;互相传值 组件之间的传值&#xff0c;分为3种方式 一.父传子&#xff0c;父组件向子组件传值 1.建立一个默认的…

零信任深入理解--概念,架构和实现方案

1. 零信任究竟解决了什么问题&#xff1f; 很多人在初步了解“零信任”概念&#xff0c;乃至进一步研究一些技术和产品方案之后&#xff0c;会有种“不过如此”的感觉。 毕竟&#xff0c; IAM&#xff08;Identity and Access Management&#xff0c;身份识别与访问管理&…

6. 【gRPC系列学习】Balance原理详解

本节开始分析Balance执行原理,通过上面流程可知,Balance Build的创建来自于Resolver的解析完成后调用updateResolverState方法,我们从updateResolverState方法开始分析Balance执行流程。 1. Balance调用流程 1)updateResolverState(s resolver.State, err error) error 方…

Java培训MySQL之全局序列

1.本地文件 不推荐&#xff0c;存在宕机序列丢失问题。 2.数据库方式 数据库序列方式原理 利用数据库一个表 来进行计数累加。但是并不是每次生成序列都读写数据库&#xff0c;这样效率太低。 mycat会预加载一部分号段到mycat的内存中&#xff0c;这样大部分读写序列都是在…

第二章:Linux的目录结构-[基础篇]

一&#xff1a;基础介绍 linux的文件系统是采用级层式的数状目录结构&#xff0c;在此结构中的最上层是根目录“/”&#xff0c;然后在此目录下再创建其他的目录。 深刻理解linux树状文件目录是非常重要的&#xff0c;这里我给大家说明一下。 记住一句经典的话&#xff1a;在Li…

通信原理循环码

目录 码多项式 码多项式的按模运算 循环码的码多项式 循环码的生成矩阵 如何寻求任一循环码循环码的生成多项式​ 码多项式 一个长度为的码组可表示成如下多项式形式&#xff1a; 多项式的系数就是码组中的各码元&#xff0c;仅是码元位置标记 。 n7 时&#xff1a; 例&…

Activiti任务的处理以及进阶使用

1.什么是流程实例 流程实例&#xff08;ProcessInstance&#xff09;代表流程定义的执行实例 一个流程实例包括所有的运行节点Task&#xff0c;所以我们一般使用来了解当前流程的进度信息 taskService.createTaskQuery().processDefinitionKey(key)例如&#xff1a;用户或者程…

力扣11.盛最多水的容器(双指针解法)

问题描述: 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容…

Mini MP3 Player播放器简介与STC12例程

文章目录1、DFRobot arduino DFPlayer官方资料1-1、简介2-2、 基本功能详述1-3、 用途1-4、 引脚说明1-5、工作模式1-5-1、 串口工作模式1-5-2、 ADC按键工作模式1-5-3、 普通按键工作模式2、串口模式电路搭建与例程参考文献1、DFRobot arduino DFPlayer官方资料 1-1、简介 Th…

KingbaseES运维案例之---服务进程(backend process)终止

​ 案例说明&#xff1a; 如下图所示&#xff1a;KingbaseES服务进程结构 KingbaseES使用客户端/服务器的模型。 对于每个客户端的连接&#xff0c;KingbaseES主进程接收到客户端连接后&#xff0c;会为其创建一个新的服务进程。 KingbaseES 用服务进程来处理连接到数据库服务的…

Java Swing JTextField:单行文本框组件

Swing 中使用 JTextField 类实现一个单行文本框&#xff0c;它允许用户输入单行的文本信息。该类的常用构造方法如下。 JTextField()&#xff1a;创建一个默认的文本框。JTextField(String text)&#xff1a;创建一个指定初始化文本信息的文本框。JTextField(int columns)&…

Nacos学习笔记 (4)Nacos整合SpringBoot流程

前提&#xff0c;先下载Nacos并启动 Nacos Server。 1. Nacos 融合 Spring Boot 为注册配置中心 实现&#xff1a; 通过 Nacos Server 和 nacos-config-spring-boot-starter 实现配置的动态变更&#xff1b;通过 Nacos Server 和 nacos-discovery-spring-boot-starter 实现服…

OpenAI 3D 模型生成器Point-E极速体验

OpenAI 3D 模型生成器Point-E极速体验 3090显卡&#xff0c;极速体验三维模型生成&#xff0c;体验地址&#xff1a;Gradio 文本生成图像的 AI 最近已经火到了圈外&#xff0c;不论是 DALL-E 2、DeepAI 还是 Stable Diffusion&#xff0c;人人都在调用 AI 算法搞绘画艺术&…

Unreal Engine工程项目目录及对应作用

目录 .vs Binaries&#xff1a;编译文件 Config&#xff1a;配置文件 Content&#xff1a;资产文件 DerivedDataCache&#xff1a;UE针对平台特化的资源版本 Intermediate&#xff1a;中间文件 Saved&#xff1a;自动保存的内容 Source&#xff1a;源&#xff08;代码&…

ADI Blackfin DSP处理器-BF533的开发详解66:MP3解码(含源码)

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 代码实现功能 代码实现了读取工程目录下的一个 MP3 文件&#xff0c;对文件进行解码后&#xff0c;将生成的数据以 PCM 文件的形式保存在工程目录…

关于数组的一些题---获取数组对象的各种数据

关于数组的一些题—获取数组对象的各种数据 题目1:将数组对象中的属性值提出来生成新的对象 var arr [{label:男&#xff0c;value: 1}, {label:女&#xff0c;value: 0}]function f(arr) {// 写代码&#xff0c;得到 } var obj f(arr); console.log(obj) // obj {1: 男&am…