一、什么是幂等性?
简单来说,就是对一个接口执行重复的多次请求,与一次请求所产生的结果是相同的,听起来非常容易理解,但要真正的在系统中要始终保持这个目标,是需要很严谨的设计的,在实际的生产环境下,我们应该保证任何接口都是幂等的,而如何正确的实现幂等,就是本文要讨论的内容。
基于 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 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
三、为什么需要幂等
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值。
带着token值,第一次请求成功。
第二次请求失败。
-
接口
+关注
关注
33文章
8575浏览量
151015 -
RPC
+关注
关注
0文章
111浏览量
11529 -
管理系统
+关注
关注
1文章
2485浏览量
35903
原文标题:一个注解,优雅的实现接口幂等性!
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论