冪等性
今天我們來(lái)談?wù)勈裁词莾绲刃裕?/p>
引用百度百科的解析如下:
冪等(idempotent、idempotence)是一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念,常見(jiàn)于抽象代數(shù)中。
在編程中一個(gè)冪等操作的特點(diǎn)是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)。這些函數(shù)不會(huì)影響系統(tǒng)狀態(tài),也不用擔(dān)心重復(fù)執(zhí)行會(huì)對(duì)系統(tǒng)造成改變。例如,“setTrue()”函數(shù)就是一個(gè)冪等函數(shù),無(wú)論多次執(zhí)行,其結(jié)果都是一樣的.更復(fù)雜的操作冪等保證是利用唯一交易號(hào)(流水號(hào))實(shí)現(xiàn)。
這解析,確實(shí)有點(diǎn)長(zhǎng)了,大家話看看就行了?。?!(●'?'●)
那對(duì)于我們程序員來(lái)說(shuō),我們關(guān)心的更多是下面這些問(wèn)題:
什么地方,什么場(chǎng)景下需要用到冪等?
冪等,我們需要怎么做,如何實(shí)現(xiàn)冪等呢?
如何實(shí)現(xiàn)冪等呢
基于 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/
什么場(chǎng)景下需要用到冪等
前端表單重復(fù)提交問(wèn)題
用戶訂單支付問(wèn)題
銀行業(yè)務(wù)辦理取號(hào)問(wèn)題
用戶惡意進(jìn)行調(diào)接口問(wèn)題
接口超時(shí)重復(fù)提交問(wèn)題
MQ消息進(jìn)行重復(fù)消費(fèi)
當(dāng)然了,還有很多場(chǎng)景會(huì)用到冪等,這里咱們就不一一列舉出來(lái)了。
那我們要如何設(shè)計(jì)一個(gè)冪等功能呢,而且還是代碼非侵入式?
代碼非侵入式的意思,就是,我們的業(yè)務(wù)邏輯代碼,不需要處理冪等校驗(yàn)的邏輯。
業(yè)務(wù)功能不處理?那交給誰(shuí)處理呢?別著急,聽(tīng)哥們一一道來(lái)。^_^
業(yè)務(wù)功能不處理?
這里,要實(shí)現(xiàn)代碼非侵入式的冪等校驗(yàn),我們就要使用到切面編程了(@Aspect)
基于 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)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
冪等的實(shí)現(xiàn)原理
在系統(tǒng)中一些接口需要增加冪等處理,冪等的概念是一個(gè)業(yè)務(wù)請(qǐng)求只能執(zhí)行一次。類似銀行業(yè)務(wù)辦理,首先需要取一個(gè)號(hào),然后用戶使用這個(gè)號(hào)去柜臺(tái)辦理業(yè)務(wù)。這個(gè)號(hào)只能使用一次,如果過(guò)期或者已辦理這個(gè)號(hào)就無(wú)效了。
我們的冪等也是使用這種原理。
1.首先客戶端調(diào)用通過(guò)我們的系統(tǒng)獲取一個(gè)號(hào),我們稱之為冪等號(hào),這個(gè)號(hào)已經(jīng)存在我們的系統(tǒng)中。
2.客戶端使用這個(gè)號(hào),調(diào)用我們的接口。
3.我們系統(tǒng)判斷這個(gè)號(hào)在我們的系統(tǒng)中已經(jīng)存在,如果存在則允許業(yè)務(wù)辦理,如果不存在,則表示這是一個(gè)非法的號(hào),我們直接拋出異常。
4.當(dāng)業(yè)務(wù)處理完成,我們會(huì)將這個(gè)號(hào)從我們的系統(tǒng)中刪除掉。
好了,這實(shí)現(xiàn)步驟,也是十分清晰了呀?。?!^_^
那么我們下面就來(lái)看代碼如何實(shí)現(xiàn)了
冪等的代碼實(shí)現(xiàn)
定義一個(gè)冪等處理接口
public?interface?Idempotence?{ ????/** ?????*?檢查是否存在冪等號(hào) ?????*?@param?idempotenceId?冪等號(hào) ?????*?@return?是否存在 ?????*/ ????boolean?check(String?idempotenceId); ????/** ?????*?記錄冪等號(hào) ?????*?@param?idempotenceId?冪等號(hào) ?????*/ ????void?record(String?idempotenceId); ????/** ?????*?記錄冪等號(hào) ?????*?@param?idempotenceId?冪等號(hào) ?????*?@param?time?過(guò)期時(shí)間 ?????*/ ????void?record(String?idempotenceId,?Integer?time); ????/** ?????*?刪除冪等號(hào) ?????*?@param?idempotenceId?冪等號(hào) ?????*/ ????void?delete(String?idempotenceId); }
定義一個(gè)冪等處理接口實(shí)現(xiàn)類
@Component public?class?RedisIdempotence?implements?Idempotence?{ ????@Autowired ????private?RedisRepository?redisRepository; ????@Override ????public?boolean?check(String?idempotenceId)?{ ????????return?redisRepository.exists(idempotenceId); ????} ????@Override ????public?void?record(String?idempotenceId)?{ ????????redisRepository.set(idempotenceId,"1"); ????} ????@Override ????public?void?record(String?idempotenceId,Integer?time)?{ ????????redisRepository.setExpire(idempotenceId,"1",time); ????} ????@Override ????public?void?delete(String?idempotenceId)?{ ????????redisRepository.del(idempotenceId); ????} }
這個(gè)實(shí)現(xiàn)類,咱們就用redis存儲(chǔ)這個(gè)冪等號(hào) 實(shí)現(xiàn)4個(gè)方法:
檢查是否存在冪等號(hào)
記錄冪等號(hào)
記錄冪等號(hào)(帶過(guò)期時(shí)間)
刪除冪等號(hào)
冪等工具類
@Component public?class?IdempotenceUtil?{ ????@Autowired ????private?RedisRepository?redisRepository; ????/** ?????*?生成冪等號(hào) ?????*?@return ?????*/ ????public?String?generateId()?{ ????????String?uuid?=?UUID.randomUUID().toString(); ????????String?uId=Base64Util.encode(uuid).toLowerCase(); ????????redisRepository.setExpire(uId,"1",1800); ????????return?uId; ????} ????/** ?????*?從Header里面獲取冪等號(hào) ?????*?@return ?????*/ ????public?String?getHeaderIdempotenceId(){ ????????ServletRequestAttributes?attributes?=?(ServletRequestAttributes)?RequestContextHolder.getRequestAttributes(); ????????HttpServletRequest?request?=?attributes.getRequest(); ????????String?idempotenceId=request.getHeader("idempotenceId"); ????????return?idempotenceId; ????} }
這個(gè)工具類,提供兩個(gè)方法。
1.生成一個(gè)冪等號(hào),咱們就用uuid
2.從Header里面獲取冪等號(hào)
定義一個(gè)注解
/** ?*?接口增加冪等性 ?*/ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public?@interface?IdempotenceRequired?{ }
切面
@Aspect @Slf4j @Component public?class?IdempotenceSupportAdvice?{ ????@Autowired ????private?Idempotence?idempotence; ????@Autowired ????IdempotenceUtil?idempotenceUtil; ????/** ?????*?攔截有@IdempotenceRequired?注解?的方法。 ?????*/ ????@Pointcut("@annotation(xxx.xxx.IdempotenceRequired)") ????public?void?idempotenceMethod(){} ????@AfterThrowing(value?=?"idempotenceMethod()()",throwing?=?"e") ????public?void?afterThrowing(Throwable?e){ ????????if(!(e?instanceof?IdempotencyException))?{ ????????????//?從HTTP?header中獲取冪等號(hào)idempotenceId ????????????String?idempotenceId?=?idempotenceUtil.getHeaderIdempotenceId(); ????????????idempotence.record(idempotenceId,?1800); ????????} ????} ????@Around(value?=?"idempotenceMethod()") ????public?Object?around(ProceedingJoinPoint??joinPoint)?throws?Throwable?{ ????????//?從HTTP?header中獲取冪等號(hào)idempotenceId ????????String?idempotenceId?=?idempotenceUtil.getHeaderIdempotenceId(); ????????if(StringUtils.isEmpty(idempotenceId)){ ????????????//不存在冪等號(hào)則不進(jìn)行額外操作 ????????????return?joinPoint.proceed(); ????????} ????????//?前置操作?冪等號(hào)是否存在 ????????boolean?existed?=?idempotence.check(idempotenceId); ????????if?(!existed)?{ ????????????throw?new?IdempotencyException("{success:false,message:"操作重復(fù),請(qǐng)重新輸入冪等號(hào)重試!",data:-2}"); ????????} ????????//刪除冪等號(hào) ????????idempotence.delete(idempotenceId); ????????Object?result?=?joinPoint.proceed(); ????????return?result; ????} }
定義個(gè)controller
@RequestMapping("/idempotence") public?class?IdempotenceController?{ ????/** ?????*?生成冪等號(hào) ?????*?@return ?????*/ ????@GetMapping("/generateId") ????public?JsonResult?generateId(){ ????????IdempotenceUtil?idempotenceUtil=SpringUtil.getBean(IdempotenceUtil.class); ????????String?uId=idempotenceUtil.generateId(); ????????return?JsonResult.success("成功生成!").setData(uId); ????} }
好了,實(shí)現(xiàn)的代碼,就是這些了,理解起來(lái)也是比較簡(jiǎn)單,沒(méi)有過(guò)多復(fù)雜的邏輯。
接下來(lái),就是如何使用的問(wèn)題了,
使用的問(wèn)題
這個(gè)使用,也是十分的簡(jiǎn)單啦?。?!
冪等的使用
「服務(wù)端:」
不是所有的方法都需要切面攔截 ,只有 IdempotenceRequired 注解的方法才會(huì)被攔截。
例如下面接口:
@IdempotenceRequired @PostMapping("/getUsers") public?JsonResult?getUsers(){ ????//執(zhí)行正常業(yè)務(wù)邏輯 ????... }
在開(kāi)發(fā)冪等接口時(shí),只需要在方法上簡(jiǎn)單增加一個(gè) IdempotenceRequired 注解即可。
這基本上就是代碼非侵入式了呀?。?!
代碼非侵入式
「客戶端:」
服務(wù)端處理好后,在客戶端訪問(wèn)接口的時(shí)候需要執(zhí)行以下步驟:
需要先獲取冪等號(hào)
然后將冪等號(hào)添加到請(qǐng)求頭中
1.獲取冪等號(hào)http://服務(wù)地址/idempotence/generateIdhttp://xn--zfry9hnb732h/idempotence/generateId
獲取冪等號(hào)
2.請(qǐng)求調(diào)用
往header中添加冪等號(hào)
往header中添加冪等號(hào)
好了,到這里冪等的實(shí)現(xiàn),就已經(jīng)完成了?。?!^_^
那我們就可以愉快的編寫(xiě)代碼了?。?!^_^
編輯:黃飛
評(píng)論
查看更多