一、什么是冪等性?
簡(jiǎn)單來說,就是對(duì)一個(gè)接口執(zhí)行重復(fù)的多次請(qǐng)求,與一次請(qǐng)求所產(chǎn)生的結(jié)果是相同的,聽起來非常容易理解,但要真正的在系統(tǒng)中要始終保持這個(gè)目標(biāo),是需要很嚴(yán)謹(jǐn)?shù)脑O(shè)計(jì)的,在實(shí)際的生產(chǎn)環(huán)境下,我們應(yīng)該保證任何接口都是冪等的,而如何正確的實(shí)現(xiàn)冪等,就是本文要討論的內(nèi)容。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
二、哪些請(qǐng)求天生就是冪等的?
首先,我們要知道查詢類的請(qǐng)求一般都是天然冪等的,除此之外,刪除請(qǐng)求在大多數(shù)情況下也是冪等的,但是ABA場(chǎng)景下除外。
舉一個(gè)簡(jiǎn)單的例子
比如,先請(qǐng)求了一次刪除A的操作,但由于響應(yīng)超時(shí),又自動(dòng)請(qǐng)求了一次刪除A的操作,如果在兩次請(qǐng)求之間,又插入了一次A,而實(shí)際上新插入的這一次A,是不應(yīng)該被刪除的,這就是ABA問題,不過,在大多數(shù)業(yè)務(wù)場(chǎng)景中,ABA問題都是可以忽略的。
除了查詢和刪除之外,還有更新操作,同樣的更新操作在大多數(shù)場(chǎng)景下也是天然冪等的,其例外是也會(huì)存在ABA的問題,更重要的是,比如執(zhí)行update table set a = a + 1 where v = 1
這樣的更新就非冪等了。
最后,就還剩插入了,插入大多數(shù)情況下都是非冪等的,除非是利用數(shù)據(jù)庫(kù)唯一索引來保證數(shù)據(jù)不會(huì)重復(fù)產(chǎn)生。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
三、為什么需要冪等
1.超時(shí)重試
當(dāng)發(fā)起一次RPC請(qǐng)求時(shí),難免會(huì)因?yàn)?a href="http://wenjunhu.com/v/tag/1722/" target="_blank">網(wǎng)絡(luò)不穩(wěn)定而導(dǎo)致請(qǐng)求失敗,一般遇到這樣的問題我們希望能夠重新請(qǐng)求一次,正常情況下沒有問題,但有時(shí)請(qǐng)求實(shí)際上已經(jīng)發(fā)出去了,只是在請(qǐng)求響應(yīng)時(shí)網(wǎng)絡(luò)異常或者超時(shí),此時(shí),請(qǐng)求方如果再重新發(fā)起一次請(qǐng)求,那被請(qǐng)求方就需要保證冪等了。
2.異步回調(diào)
異步回調(diào)是提升系統(tǒng)接口吞吐量的一種常用方式,很明顯,此類接口一定是需要保證冪等性的。
3.消息隊(duì)列
現(xiàn)在常用的消息隊(duì)列框架,比如:Kafka、RocketMQ、RabbitMQ在消息傳遞時(shí)都會(huì)采取At least once原則(也就是至少一次原則,在消息傳遞時(shí),不允許丟消息,但是允許有重復(fù)的消息),既然消息隊(duì)列不保證不會(huì)出現(xiàn)重復(fù)的消息,那消費(fèi)者自然要保證處理邏輯的冪等性了。
四、實(shí)現(xiàn)冪等的關(guān)鍵因素
關(guān)鍵因素1
冪等唯一標(biāo)識(shí),可以叫它冪等號(hào)或者冪等令牌或者全局ID,總之就是客戶端與服務(wù)端一次請(qǐng)求時(shí)的唯一標(biāo)識(shí),一般情況下由客戶端來生成,也可以讓第三方來統(tǒng)一分配。
關(guān)鍵因素2
有了唯一標(biāo)識(shí)以后,服務(wù)端只需要確保這個(gè)唯一標(biāo)識(shí)只被使用一次即可,一種常見的方式就是利用數(shù)據(jù)庫(kù)的唯一索引。
五、注解實(shí)現(xiàn)冪等性
下面演示一種利用Redis來實(shí)現(xiàn)的方式。
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{
/**
*參數(shù)名,表示將從哪個(gè)參數(shù)中獲取屬性值。
*獲取到的屬性值將作為KEY。
*
*@return
*/
Stringname()default"";
/**
*屬性,表示將獲取哪個(gè)屬性的值。
*
*@return
*/
Stringfield()default"";
/**
*參數(shù)類型
*
*@return
*/
Classtype();
}
2.統(tǒng)一的請(qǐng)求入?yún)?duì)象
@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"重復(fù)請(qǐng)求";
}
}
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;ireturnparam;
}
}
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. 請(qǐng)求示例
調(diào)用接口之前,先申請(qǐng)一個(gè)token,然后帶著服務(wù)端返回的token值,再去請(qǐng)求。
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";
}
}
請(qǐng)求獲取token值。
帶著token值,第一次請(qǐng)求成功。
第二次請(qǐng)求失敗。
-
接口
+關(guān)注
關(guān)注
33文章
8615瀏覽量
151307 -
RPC
+關(guān)注
關(guān)注
0文章
111瀏覽量
11540 -
管理系統(tǒng)
+關(guān)注
關(guān)注
1文章
2521瀏覽量
35946
原文標(biāo)題:一個(gè)注解,優(yōu)雅的實(shí)現(xiàn)接口冪等性!
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論