0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一个注解,优雅的实现接口幂等性!

jf_ro2CN3Fa 来源:芋道源码 2023-08-26 14:36 次阅读


一、什么是幂等性?

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

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

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

举一个简单的例子

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

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

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

三、为什么需要幂等

1.超时重试

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

2.异步回调

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

3.消息队列

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

四、实现幂等的关键因素

关键因素1

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

关键因素2

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

五、注解实现幂等性

下面演示一种利用Redis来实现的方式。

1.自定义注解

importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;

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

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

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

/**
*参数类型
*
*@return
*/
Classtype();

}

2.统一的请求入参对象

@Data
publicclassRequestData<T>{

privateHeaderheader;

privateTbody;

}


@Data
publicclassHeader{

privateStringtoken;

}

@Data
publicclassOrder{

StringorderNo;

}

3.AOP处理

importcom.springboot.micrometer.annotation.Idempotent;
importcom.springboot.micrometer.entity.RequestData;
importcom.springboot.micrometer.idempotent.RedisIdempotentStorage;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.annotation.Pointcut;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.springframework.stereotype.Component;

importjavax.annotation.Resource;
importjava.lang.reflect.Method;
importjava.util.Map;

@Aspect
@Component
publicclassIdempotentAspect{

@Resource
privateRedisIdempotentStorageredisIdempotentStorage;

@Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
publicvoididempotent(){
}

@Around("idempotent()")
publicObjectmethodAround(ProceedingJoinPointjoinPoint)throwsThrowable{
MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();
Methodmethod=signature.getMethod();
Idempotentidempotent=method.getAnnotation(Idempotent.class);

Stringfield=idempotent.field();
Stringname=idempotent.name();
ClassclazzType=idempotent.type();

Stringtoken="";

Objectobject=clazzType.newInstance();
MapparamValue=AopUtils.getParamValue(joinPoint);
if(objectinstanceofRequestData){
RequestDataidempotentEntity=(RequestData)paramValue.get(name);
token=String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(),field));
}

if(redisIdempotentStorage.delete(token)){
returnjoinPoint.proceed();
}
return"重复请求";
}
}
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.reflect.CodeSignature;

importjava.lang.reflect.Field;
importjava.util.HashMap;
importjava.util.Map;

publicclassAopUtils{

publicstaticObjectgetFieldValue(Objectobj,Stringname)throwsException{
Field[]fields=obj.getClass().getDeclaredFields();
Objectobject=null;
for(Fieldfield:fields){
field.setAccessible(true);
if(field.getName().toUpperCase().equals(name.toUpperCase())){
object=field.get(obj);
break;
}
}
returnobject;
}


publicstaticMapgetParamValue(ProceedingJoinPointjoinPoint){
Object[]paramValues=joinPoint.getArgs();
String[]paramNames=((CodeSignature)joinPoint.getSignature()).getParameterNames();
Mapparam=newHashMap<>(paramNames.length);

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

4.Token值生成

importcom.springboot.micrometer.idempotent.RedisIdempotentStorage;
importcom.springboot.micrometer.util.IdGeneratorUtil;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;

importjavax.annotation.Resource;

@RestController
@RequestMapping("/idGenerator")
publicclassIdGeneratorController{

@Resource
privateRedisIdempotentStorageredisIdempotentStorage;

@RequestMapping("/getIdGeneratorToken")
publicStringgetIdGeneratorToken(){
StringgenerateId=IdGeneratorUtil.generateId();
redisIdempotentStorage.save(generateId);
returngenerateId;
}

}
publicinterfaceIdempotentStorage{

voidsave(StringidempotentId);

booleandelete(StringidempotentId);
}
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Component;

importjavax.annotation.Resource;
importjava.io.Serializable;
importjava.util.concurrent.TimeUnit;

@Component
publicclassRedisIdempotentStorageimplementsIdempotentStorage{

@Resource
privateRedisTemplateredisTemplate;

@Override
publicvoidsave(StringidempotentId){
redisTemplate.opsForValue().set(idempotentId,idempotentId,10,TimeUnit.MINUTES);
}

@Override
publicbooleandelete(StringidempotentId){
returnredisTemplate.delete(idempotentId);
}
}
importjava.util.UUID;

publicclassIdGeneratorUtil{

publicstaticStringgenerateId(){
returnUUID.randomUUID().toString();
}

}

5. 请求示例

调用接口之前,先申请一个token,然后带着服务端返回的token值,再去请求。

importcom.springboot.micrometer.annotation.Idempotent;
importcom.springboot.micrometer.entity.Order;
importcom.springboot.micrometer.entity.RequestData;
importorg.springframework.web.bind.annotation.RequestBody;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
publicclassOrderController{

@RequestMapping("/saveOrder")
@Idempotent(name="requestData",type=RequestData.class,field="token")
publicStringsaveOrder(@RequestBodyRequestDatarequestData){
return"success";
}

}

请求获取token值。

533d7310-43cc-11ee-a2ef-92fbcf53809c.png

带着token值,第一次请求成功。

53540ddc-43cc-11ee-a2ef-92fbcf53809c.png

第二次请求失败。

53676bde-43cc-11ee-a2ef-92fbcf53809c.png


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8467

    浏览量

    150757
  • RPC
    RPC
    +关注

    关注

    0

    文章

    111

    浏览量

    11496
  • 管理系统
    +关注

    关注

    1

    文章

    2385

    浏览量

    35780

原文标题:一个注解,优雅的实现接口幂等性!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    离线计算中的和DataWorks中的相关事项

    多次相同的消息,针对同笔交易的付款也不应该在重试过程中扣多次钱。曾见过案例,有对于
    发表于 02-27 13:24

    在高并发下怎么保证接口

    前言 接口性问题,对于开发人员来说,是跟语言无关的公共问题。本文分享了些解决这类问题非
    的头像 发表于 05-14 10:23 1766次阅读
    在高并发下怎么保证<b class='flag-5'>接口</b>的<b class='flag-5'>幂</b><b class='flag-5'>等</b><b class='flag-5'>性</b>?

    注解定义Bean及开发

    注解本质是继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。
    发表于 08-02 10:26 428次阅读

    什么是?关于接口的解决方案

    这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加version字段,当数据需要更新时,先去数据库里获取此时的version版本号
    发表于 10-09 10:19 1900次阅读

    分析解决)的方法

    这个概念,是数学上的概念,即:f……(f(f(x))) = f(x)。用在计算机领域,指的是系统里的接口或方法对外的
    的头像 发表于 10-14 10:08 906次阅读

    Spring Boot实现接口的4种方案

    数学与计算机学概念,在数学中某元运算为
    的头像 发表于 11-08 10:21 967次阅读

    如何设计优雅的API接口

    种是API接口提供方给出AK/SK两值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口
    的头像 发表于 12-20 14:23 1571次阅读

    什么是实现原理

    在编程中操作的特点是其任意多次执行所产生的影响均与次执行的影响相同。
    发表于 01-05 10:40 6000次阅读

    给定接口,要用户自定义动态实现并上传热部署

    考虑到用户实现接口的两种方式,使用spring上下文管理的方式,或者不依赖spring管理的方式,这里称它们为注解方式和反射方式。calculate方法对应注解方式,add方法对应反射
    的头像 发表于 01-06 14:14 521次阅读

    如何实现注解进行数据脱敏

    、测试 后记   本文主要分享什么是数据脱敏,如何优雅的在项目中运用注解实现数据脱敏,为项目进行赋能。希望能给你们带来帮助。 什么是数据
    的头像 发表于 06-14 09:37 971次阅读
    如何<b class='flag-5'>实现</b><b class='flag-5'>一</b><b class='flag-5'>个</b><b class='flag-5'>注解</b>进行数据脱敏

    基于接口解决方案

    接口是指无论调用接口的次数是次还是多次,对于同
    的头像 发表于 09-30 16:27 407次阅读
    基于<b class='flag-5'>接口</b><b class='flag-5'>幂</b><b class='flag-5'>等</b><b class='flag-5'>性</b>解决方案

    和非请求的些定义和分析

    最近在做项目的过程中,有需求是在客户端 HTTP 请求失败后,增加重试机制,然后我就翻了些有关“重试”的库,找到
    的头像 发表于 10-17 10:50 734次阅读

    接口异常优雅处理介绍及实战

    Spring在3.2版本增加了注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAttribute
    的头像 发表于 10-22 16:01 688次阅读
    <b class='flag-5'>接口</b>统<b class='flag-5'>一</b>异常<b class='flag-5'>优雅</b>处理介绍及实战

    为什么要实现校验 如何实现接口校验

    前端重复提交表单:在填写些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后直点提交按钮,这时就会发生重复提交表单请求。
    的头像 发表于 02-20 14:14 1114次阅读

    探索LabVIEW编程接口原理与实践

    原来是数学上的概念,在编程领域可以理解为:多次请求某一个资源或执行某一个操作时应该具有唯一性
    的头像 发表于 02-29 10:24 549次阅读
    探索LabVIEW编程<b class='flag-5'>接口</b><b class='flag-5'>幂</b><b class='flag-5'>等</b><b class='flag-5'>性</b>原理与实践