自定义注解实现幂等

image.png

在前面的文章中,我们说过解决消息重复消费的方式中,有一个方式是幂等,那么幂等是怎么实现呢?

面试官:对于MQ中的消息重复消费说说的你的理解

一、定义

首先我们先来了解一下幂等的定义,它指的是同一个操作的重复执行不会产生额外的影响,也就是多次执行与一次执行的结果效果相同。

二、影响

当方法不是幂等的时候,对于我们的系统会产生很多的影响,例如:

  • 重复调用造成资源浪费。
  • 数据不一致。
  • 业务逻辑发生错误。

所以我们在写接口时,一定要注意接口的幂等,保障接口幂等,相当于保住自己的饭碗😂(尤其涉及到 money 的系统)。

三、自定义注解实现幂等

在使用注解实现幂等之前,先说一下大概思路。

这个思路与分布式锁大体相同,所以理解起来会相对容易点,需要注意的就是释放的时机

image.png

  1. AOP 拦截需要做幂等的方法。
  2. 获取 key 的解析器。
  3. 通过 key 解析器解析出来判断幂等的条件(也就是什么条件下才算是重复的请求)。
  4. 在 Redis 中判断该 key 是否存在。
  5. 如果存在,说明已经有在执行的请求,直接拒绝请求,响应结束。
  6. 如果不存在,说明当前线程是首次请求,放行请求,开始执行方法。

上述流程很简单吧,如果你看懂了就跟我一起来实战一下。

需要注意的是,判断幂等的条件不是唯一的,不同的业务场景可以使用不同的幂等条件,所以这个地方需要支持自定义幂等 key。

3.1、自定义注解 Idempotent

 

java

复制代码

@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent {    int timeout() default 1;    TimeUnit timeUnit() default TimeUnit.SECONDS;    String message() default "重复请求,请稍后重试";    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;    String keyArg() default "";    boolean deleteKeyWhenException() default true; }

  • timeout 指定幂等操作的超时时间,默认是 1 秒。
  • timeUnit 指定时间单位,默认SECONDS
  • message 正在执行时的提示信息。
  • keyResolver也就是我们所说的自定义 key的解析器。
  • keyArg 使用Spring EL 表达式解析器解析`key使用。
  • deleteKeyWhenException当发生异常的时候是否删除 key。发生异常的时候删除key是为了避免下次请求无法正常执行。当请求正常的时候不需要,如果删除的话,不就和开头一样了吗,分布式锁?

3.2、自定义 key 解析器

定义 key 解析器IdempotentKeyResolver

 

java

复制代码

public interface IdempotentKeyResolver { ​    /**     * 解析一个 Key     *     * @param idempotent 幂等注解     * @param joinPoint AOP 切面     * @return Key     */    String resolver(JoinPoint joinPoint, Idempotent idempotent); }

3.2.1、默认的 key 解析器

默认解析我们使用方法名加参数生成一个 key,因为参数可能过长,所以我们使用MD5压缩一下。

 

typescript

复制代码

public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { ​    @Override    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {        String methodName = joinPoint.getSignature().toString();        String argsStr = StrUtil.join(",", joinPoint.getArgs());        return SecureUtil.md5(methodName + argsStr);   } }

3.2.2、使用用户信息做 key

我们使用方法名、参数、用户ID、用户类型生成 key,同样使用 MD5 压缩。

用户ID用户类型取决于我们自己怎么获取,可以读取session也可以读取数据库,具体取决自己的业务系统,此处就不再演示。

 

ini

复制代码

public class UserIdempotentKeyResolver implements IdempotentKeyResolver { ​    @Override    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {        String methodName = joinPoint.getSignature().toString();        String argsStr = StrUtil.join(",", joinPoint.getArgs());        Long userId = "";        Integer userType = "";        return SecureUtil.md5(methodName + argsStr + userId + userType);   } ​ }

3.2.3、Spring EL 表达式解析 key

使用Spring EL表达式解析,在使用中通过 EL 表达式解析参数,最后生成一个key

 

scss

复制代码

public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { ​    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();    private final ExpressionParser expressionParser = new SpelExpressionParser(); ​    @Override    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {        // 获得被拦截方法参数名列表        Method method = getMethod(joinPoint);        Object[] args = joinPoint.getArgs();        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);        // 准备 Spring EL 表达式解析的上下文        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();        if (ArrayUtil.isNotEmpty(parameterNames)) {            for (int i = 0; i < parameterNames.length; i++) {                evaluationContext.setVariable(parameterNames[i], args[i]);           }       } ​        // 解析参数        Expression expression = expressionParser.parseExpression(idempotent.keyArg());        return expression.getValue(evaluationContext, String.class);   } ​    private static Method getMethod(JoinPoint point) {        // 处理,声明在类上的情况        MethodSignature signature = (MethodSignature) point.getSignature();        Method method = signature.getMethod();        if (!method.getDeclaringClass().isInterface()) {            return method;       } ​        // 处理,声明在接口上的情况        try {            return point.getTarget().getClass().getDeclaredMethod(                    point.getSignature().getName(), method.getParameterTypes());       } catch (NoSuchMethodException e) {            throw new RuntimeException(e);       }   } ​ }

3.3、幂等注解逻辑处理类

拦截添加了注解的方法,实现对应的幂等操作。

 

java

复制代码

@Aspect @Slf4j public class IdempotentAspect { ​    /**     * IdempotentKeyResolver 集合     */    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers; ​    private final IdempotentRedisDAO idempotentRedisDAO; ​    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);        this.idempotentRedisDAO = idempotentRedisDAO;   } ​    @Around(value = "@annotation(idempotent)")    public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {        // 获得 IdempotentKeyResolver        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");        // 解析 Key        String key = keyResolver.resolver(joinPoint, idempotent); ​        // 1. 锁定 Key        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());        // 锁定失败,抛出异常        if (!success) {            log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());       } ​        // 2. 执行逻辑        try {            return joinPoint.proceed();       } catch (Throwable throwable) {            // 3. 异常时,删除 Key            if (idempotent.deleteKeyWhenException()) {                idempotentRedisDAO.delete(key);           }            throw throwable;       }   } ​ }

3.4、封装Redis操作

对于 key的缓存,我们放在 Redis中,所以我们此处封装一个 Redis操作类。

 

typescript

复制代码

@AllArgsConstructor public class IdempotentRedisDAO { ​    /**     * 幂等操作     *     * KEY 格式:idempotent:%s // 参数为 uuid     * VALUE 格式:String     * 过期时间:不固定     */    private static final String IDEMPOTENT = "idempotent:%s"; ​    private final StringRedisTemplate redisTemplate; ​    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {        String redisKey = formatKey(key);        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);   } ​    public void delete(String key) {        String redisKey = formatKey(key);        redisTemplate.delete(redisKey);   } ​    private static String formatKey(String key) {        return String.format(IDEMPOTENT, key);   } } ​

四、使用注解 Idempotent

需要引入注解,切面,以及解析配置类,让其被Spring管理起来,然后在需要使用的接口上增加注解。

 

less

复制代码

  @Idempotent(idempotent = true,expireTime = 3,timeUnit = TimeUnit.SECONDS,info = "请勿重复更新用户密码",delKey = false)    @PutMapping(value = "updatePassword")    public String updatePassword(User user){        userServiceImpl.updatePassword(user);        return "更新成功";   }

总结

总结一下设计思路以及需要注意的地方。

  • AOP拦截请求,方法处理之前先存入Redis中 keyvalue以及过期时间。
  • 过期时间必须设置,防止一个请求阻塞,自动过期时间必须是超过业务逻辑处理时间
  • 该方案是接口请求层面的幂等,如果业务方面的,还需要业务单独开发自己本身的幂等逻辑
  • 前端请求做遮罩层,防止在过期时间小于业务处理时间时的多次触发,造成业务的不一致。
  • 对于业务的幂等数据库层面可以创建唯一索引,先查询在添加。
  • 这种方式与分布式锁逻辑类似,但是不可用于分布锁,并发压测下会有问题。但是做幂等就可以,因为实际的情况就是同一个用户不会在短短的3、5秒内完成50-100个以上的重复请求。
  • 对于 key 的生成还可以加上请求IP做限制。

好了,接口的幂等方案到这就结束了,文中的代码参考的是yudao-cloud的幂等设计,感兴趣的可以看一下。如有错误也欢迎指出,大家一起评论区交流学习。

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

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

相关文章

嵌入式Linux系统编程 — 3.7 文件目录与处理

目录 1 文件目录 1.1 文件目录简介 1.2 目录存储形式 2 创建和删除目录 2.1 mkdir创建目录 2.2 rmdir删除空目录 3 opendir打开、 readdir读取以及closedir关闭目录 3.1 打开文件 opendir 3.2 读取目录 readdir 3.3 重置目录起点rewinddir 3.4 关闭目录 closedir 3…

基于DeepNLP AI Store真人点评和ShowCase分享社区-AI for Image Generator

来源 quora 社区: https://deepnlpaistore.quora.com/ github: https://rockingdingo.github.io/deepnlp/store/image_generator 内容 DeepNLP AI Store 网址&#xff1a;http://www.deepnlp.org/store/image-generator 网站针对图像生成类别 Image Generator下多个AI工具如 …

第 28 篇 : SSH秘钥登录

1 生成秘钥 ssh-keygen -t rsa ls -a ./.ssh/一直回车就行了 2. 修改配置 vi /etc/ssh/sshd_config放开注释 公钥的位置修改 关闭密码登录 PubkeyAuthentication yes AuthorizedKeysFile .ssh/id_rsa.pub PasswordAuthentication no3. 下载id_rsa私钥, 自行解决 注意…

Websocket在Java中的实践——自动注册端点

在《Websocket在Java中的实践——握手拦截器》中我们使用握手拦截器实现了路径解析的工作。这个过程略显复杂&#xff0c;因为路径解析这样比较底层的工作应该由框架来解决&#xff0c;而不应该交由开发者来做。本文介绍的自动注册端点的功能就可以很优雅的解决这个问题。 依赖…

GNU、Unix、Linux、Makefile、GCC、GDB、GPL、CentOS 7、Ubuntu之间的关系

全文总结 早期&#xff0c;Unix系统作为一类强大的操作系统&#xff0c;在计算领域奠定了基础。然而&#xff0c;出于对软件自由的追求&#xff0c;Richard Stallman在1983年发起了GNU项目&#xff0c;旨在创建一个完全自由的、与Unix兼容的操作系统。GNU项目不仅倡议软件自由…

初创企业合规管理中的企业合规义务边界问题

在初创企业的迅猛发展过程中&#xff0c;合规管理是确保公司可持续成长和避免潜在风险的关键因素。而在合规管理中&#xff0c;界定企业边界尤为重要&#xff0c;它关系到企业如何合理规划业务范围、管理内部外部关系以及维护企业形象和法律责任的清晰。 一、初创企业面临的合…

ubuntu 18 虚拟机安装(3)安装mysql

ubuntu 18 虚拟机安装&#xff08;3&#xff09;安装mysql 参考 https://cloud.tencent.com/developer/article/1700780 技术分享 | MySQL 设置管理员密码无法生效一例 https://cloud.tencent.com/developer/article/2014384 在Ubuntu18.04上安装MySQL &#xff5c; 超级详细…

flink-触发器Trigger和移除器Evictor

Trigger 触发器 触发器作用&#xff1a;控制窗口什么时候除法计算。即执行窗口函数&#xff1b;基于WindowStream调用trigger&#xff08;&#xff09;方法&#xff0c;传入自定义触发器&#xff08;trigger&#xff09;&#xff1b; 每一个窗口分配器&#xff08;windowAssi…

如何以智能方式安装 Python

Python易于使用&#xff0c;对初学者友好&#xff0c;功能强大&#xff0c;几乎可以为任何应用程序创建强大的软件。 但与任何其他软件一样&#xff0c;Python 的设置和管理可能很复杂。 在本文中&#xff0c;我们将介绍如何正确设置 Python。 您将学习如何选择合适的版本、…

JavaScript中常用数据类型做布尔值(Boolean)转换

一、前言 二、示例 1、String转Boolean 2、Number转Boolean 3、NaN、Null、undefined 转Boolean 4、Object转Boolean 5、Array转Boolean 6、Symbol转Boolean 三、总结 四、思考 一、前言 JavaScript中&#xff0c;经常需要对一些值进行boolean判断&#xff0c;根据判…

three.js 第六节 - 纹理以及贴图【.hdr文件(hdr贴图)】- 色彩空间

素材 这是素材 更多素材、案例、项目 好几个G一共&#xff0c;加我q178373168&#xff0c;60大洋拿走 源码 源码 // ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls…

【考研数学】李林《880》25版主要变化汇总

25版李林880拿到手后对比&#xff0c;发现和24几乎没有太大差别&#xff08;高数前两章20多道题目增删&#xff09;&#xff0c;然后24又和23版本一模一样 所以有24版本or23版本的880都可以&#xff0c;不用一定追求25年的&#xff01; 880算是比较经典的习题了&#xff0c;搭…

UE引擎实现ShadowMap、体积光(C++)

前言 整体上参考了YivanLee大佬的这两篇文&#xff1a; 虚幻4渲染编程&#xff08;灯光篇&#xff09;【第一卷&#xff1a;各种ShadowMap】 虚幻4渲染编程&#xff08;灯光篇&#xff09;【第二卷&#xff1a;体积光】 正文 1、ShadowMap &#xff08;1&#xff09;创建工…

上下文管理器在Python中的妙用

更多Python学习内容&#xff1a;ipengtao.com Python上下文管理器是一个非常强大的工具&#xff0c;它能够帮助开发者在特定代码块前后自动执行特定的操作&#xff0c;常用于资源管理&#xff0c;如文件操作、数据库连接和锁定等。本文将详细介绍Python上下文管理器的概念、使用…

django学习入门系列之第三点《案例 商品推荐部分》

文章目录 划分区域搭建骨架完整代码小结往期回顾 划分区域 搭建骨架 /*商品图片&#xff0c;父级设置*/ .slider .sd-img{display: block;width: 1226px;height: 460px; }<!-- 商品推荐部分 --> <!--搭建出一个骨架--> <div class"slider"><di…

云计算基础技术

云计算基础技术概览 计算类产品主要提供算力&#xff0c;支持业务运行&#xff0c;例如网站、办公软件、数据分析等计算能力&#xff0c;目前典型的产品主要是虚拟化和容器&#xff0c;在公有云上的云主机本质也是虚拟机。网络类产品主要满足资源的网络连通性和隔离&#xff0c…

鸿蒙NEXT开发:工具常用命令—install

安装三方库。 命令格式 ohpm install [options] [[<group>/]<pkg>[<version> | tag:<tag>]] ... ohpm install [options] <folder> ohpm install [options] <har file> alias: i 说明 group&#xff1a;三方库的命名空间&#xff0c;可…

告别数据线!轻松实现iOS和安卓设备间的文件共享

用 AirDroid 的附近传输功能&#xff0c;完全免费&#xff0c;几十个G的文件也可以相互传输。不限制iPhone和iPad数量&#xff0c;多个设备同时登录也不会强迫下线。 当你要在苹果手机和安卓手机之间传输文件&#xff0c;请将AirDroid安装到两台手机上&#xff0c;然后登录同一…

Open3D(C++) 删除点云中重复的点

目录 一、算法原理1、重叠点2、主要函数二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫与GPT。 一、算法原理 1、重叠点 原始点云克隆一份   构造重叠区域   合并点云获得重叠点 2、主要…

【Mysql】多表、外键约束

多表 1.1 多表简述 实际开发中&#xff0c;一个项目通常需要很多张表才能完成。 例如一个商城项目的数据库,需要有很多张表&#xff1a;用户表、分类表、商品表、订单表… 1.2 单表的缺点 1.2.1 数据准备 创建一个数据库 db3 CREATE DATABASE db3 CHARACTER SET utf8;数据库…