1. 概覽
在分布式場(chǎng)景中,Retry 和 Fallback 是最常見的容災(zāi)方案。
Retry 就是在調(diào)用遠(yuǎn)程接口失敗時(shí),Client 主動(dòng)發(fā)起重試請(qǐng)求,以期待獲得最終結(jié)果,從而完成整個(gè)流程
Fallback 是在調(diào)用遠(yuǎn)程接口失敗時(shí),Client 不進(jìn)行重試而是調(diào)用一個(gè)特殊的 fallback 方法,從這個(gè)方法中獲取結(jié)果,使流程能夠繼續(xù)下去
那 Retry 和 Fallback 該怎么抉擇呢?
1.1. 背景
首先,先看下 Retry 和 Fallback 都是怎么幫助流程進(jìn)行自我恢復(fù)的。
1.1.1. Retry
現(xiàn)在有一個(gè)生單流程:
核心流程如下:
從商品服務(wù)中獲取商品信息
根據(jù)商品信息創(chuàng)建訂單
將訂單保存到數(shù)據(jù)庫
如果發(fā)生網(wǎng)絡(luò)抖動(dòng),將導(dǎo)致生單失敗。
在調(diào)用商品服務(wù)獲取商品時(shí),由于網(wǎng)絡(luò)異常,接口調(diào)用失敗
由于無法獲取商品信息,生單流程被異常中斷
由于生單流程太過重要,系統(tǒng)需盡最大努力保障用戶能夠完成下單操作,那針對(duì)網(wǎng)絡(luò)抖動(dòng)這個(gè)問題,可以通過 Retry 進(jìn)行修復(fù)。
在第一次獲取商品信息時(shí),由于網(wǎng)絡(luò)問題導(dǎo)致獲取失敗
系統(tǒng)不會(huì)直接拋出異常,而是在等待一段時(shí)間后,重新發(fā)起第二次請(qǐng)求,也就是 Retry 操作
網(wǎng)絡(luò)恢復(fù),第二次請(qǐng)求成功獲取商品信息
流程繼續(xù)運(yùn)行,最終完成用戶生單
Retry 機(jī)制非常適合服務(wù)短時(shí)間不可用,或某個(gè)服務(wù)節(jié)點(diǎn)異常 這類場(chǎng)景。
1.1.2. Fallback
一個(gè)生單驗(yàn)證接口,主流程如下:
調(diào)用商品服務(wù)的接口獲取商品信息
根據(jù)商品和用戶信息判斷用戶是否能夠購買該商品
同樣,假設(shè)在訪問商品服務(wù)時(shí)出現(xiàn)網(wǎng)絡(luò)異常:
由于無法獲取商品信息,從而導(dǎo)致整個(gè)驗(yàn)證流程被異常中斷,用戶操作被迫終止。
聰明的你估計(jì)會(huì)說那就使用 Retry 呀,是的:
如果是短時(shí)不可用,通過 Retry 機(jī)制便可以恢復(fù)流程。
但,如果是商品服務(wù)壓力過大,響應(yīng)時(shí)間過長(zhǎng)呢?比如,商品服務(wù)流量激增,導(dǎo)致 DB CPU 飆升,出現(xiàn)大量的慢 SQL,這時(shí)觸發(fā)了系統(tǒng)的 Retry 會(huì)是怎樣?
在獲取商品失敗后,系統(tǒng)自動(dòng)觸發(fā) Retry 機(jī)制
由于是商品服務(wù)本身出了問題,第二次請(qǐng)求仍舊失敗
服務(wù)又觸發(fā)了第三次請(qǐng)求,仍未獲取結(jié)果
達(dá)到最大重試次數(shù),仍舊無法獲取商品,只能通過異常中斷用戶請(qǐng)求
通過 Retry 機(jī)制未能將流程從異常中恢復(fù)過來,也給下游的 商品服務(wù) 造成了巨大傷害。
商品服務(wù)壓力大,響應(yīng)時(shí)間長(zhǎng)
上游系統(tǒng)由于超時(shí)觸發(fā)自動(dòng)重試
自動(dòng)重試增大了對(duì)商品服務(wù)的調(diào)用
商品服務(wù)請(qǐng)求量更大,更難以從故障中恢復(fù)
這就是常說的“讀放大”,假設(shè)用戶驗(yàn)證是否能夠購買請(qǐng)求的請(qǐng)求量為 n,那極端情況下 商品服務(wù)的請(qǐng)求量為 3n (其中 2n 是由 Retry 機(jī)制造成)
此時(shí),Retry 就不是一個(gè)好的方案。我們先退回業(yè)務(wù)場(chǎng)景進(jìn)行思考,如果無法獲取商品,驗(yàn)證接口是否可以直接放行,先讓用戶完成購買?
如果,這個(gè)業(yè)務(wù)假設(shè)能夠接受的話,那就到了 Fallback 上場(chǎng)的時(shí)候了。
調(diào)用商品服務(wù)獲取商品信息失敗
系統(tǒng)不會(huì)進(jìn)行重試,而是觸發(fā) fallback 機(jī)制
fallback 會(huì)調(diào)用指定的一個(gè)方法,并將返回值作為遠(yuǎn)程接口的返回值
接下來的流程使用 fallback 方法的返回值完成業(yè)務(wù)邏輯
1.1.3. 場(chǎng)景思考
同樣是對(duì)商品服務(wù)接口(同一個(gè)接口)的調(diào)用,在不同的場(chǎng)景需要使用不同的策略用以恢復(fù)業(yè)務(wù)流程,通常情況下:
Command 場(chǎng)景優(yōu)先使用 Retry
這種流量即為重要,最好能保障流程的完整性
通常寫流量比較小,小范圍 Retry 不會(huì)對(duì)下游系統(tǒng)造成巨大影響
Query 場(chǎng)景優(yōu)選使用 Fallabck
大多數(shù)展示場(chǎng)景,哪怕部分信息沒有獲取到對(duì)整體的影響也比較小
通常讀場(chǎng)景流量較高,Retry 對(duì)下游系統(tǒng)的傷害不容忽視
那面對(duì)一個(gè)遠(yuǎn)程接口被多個(gè)場(chǎng)景使用,我們?cè)撛趺刺幚砟兀?/p>
提供兩組接口,一個(gè)具有 Retry 能力,一個(gè)具有 Fallback 能力,由使用方根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行選擇?
還是…
1.2. 目標(biāo)
遠(yuǎn)程接口具備 Retry 和 Fallback 能力
能夠根據(jù)上下文不同場(chǎng)景,在發(fā)生調(diào)用異常時(shí)動(dòng)態(tài)選擇 Retry 或 Fallback 進(jìn)行流程恢復(fù)
2. 快速入門
2.1. 準(zhǔn)備環(huán)境
項(xiàng)目主要依賴 spring retry 和 lego starter首先,引入 spring-retry 依賴
org.springframework.retry spring-retry
此次,引入 lego-starter 依賴
com.geekhalo.lego lego-starter 0.1.17
最后新建 RetryConfiguration 以開啟 Retry 能力
@EnableRetry @Configuration publicclassRetryConfiguration{ }
2.2. 構(gòu)建 ActionTypeProvider
在完成基本配置后,需要準(zhǔn)備一個(gè) ActionTypeProvider 用以提供上下文信息。ActionTypeProvider 接口定義如下:
publicinterfaceActionTypeProvider{ ActionTypeget(); } publicenumActionType{ COMMAND,QUERY }
通常情況下,我們會(huì)使用 ThreadLocal 組件將 ActionType 存儲(chǔ)于線程上下文,在使用時(shí)從上下中獲取相關(guān)信息。
publicclassActionContext{ privatestaticfinalThreadLocalACTION_TYPE_THREAD_LOCAL=newThreadLocal<>(); publicstaticvoidset(ActionTypeactionType){ ACTION_TYPE_THREAD_LOCAL.set(actionType); } publicstaticActionTypeget(){ returnACTION_TYPE_THREAD_LOCAL.get(); } publicstaticvoidclear(){ ACTION_TYPE_THREAD_LOCAL.remove(); } }
有了上下文之后,ActionBasedActionTypeProvider 直接從 Context 中獲取 ActionType 具體如下
@Component publicclassActionBasedActionTypeProviderimplementsActionTypeProvider{ @Override publicActionTypeget(){ returnActionContext.get(); } }
上下文中的 ActionType 又是怎么進(jìn)行管理的呢,包括信息綁定和信息清理?最常用的方式便是:
提供一個(gè)注解,在方法上添加注解用于對(duì) ActionType 的配置;
提供一個(gè)攔截器,對(duì)方法調(diào)用進(jìn)行攔截。方法調(diào)用前,從注解中獲取配置信息并綁定到上下文;方法調(diào)用后,主動(dòng)清理上下文信息;
核心實(shí)現(xiàn)為:
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public@interfaceAction{ ActionTypetype(); } @Aspect @Component @Order(Integer.MIN_VALUE) publicclassActionAspect{ @Pointcut("@annotation(com.geekhalo.lego.faultrecovery.smart.Action)") publicvoidpointcut(){ } @Around(value="pointcut()") publicObjectaction(ProceedingJoinPointjoinPoint)throwsThrowable{ MethodSignaturemethodSignature=(MethodSignature)joinPoint.getSignature(); Actionannotation=methodSignature.getMethod().getAnnotation(Action.class); ActionContext.set(annotation.type()); try{ returnjoinPoint.proceed(); }finally{ ActionContext.clear(); } } }
在這些組件的幫助下,我們只需在方法上基于 @Action 注解進(jìn)行標(biāo)記,便能夠?qū)?ActionType 綁定到上下文。
2.3. 使用 @SmartFault
在將 ActionType 綁定到上下文之后,接下來要做的便是對(duì) 遠(yuǎn)程接口 進(jìn)行配置。遠(yuǎn)程接口的配置工作主要由 @SmartFault 來完成。其核心配置項(xiàng)包括:
配置項(xiàng) | 含義 | 默認(rèn)配置 |
---|---|---|
recover | fallback 方法名稱 | |
maxRetry | 最大重試次數(shù) | 3 |
include | 觸發(fā)重試的異常類型 | |
exclude | 不需要重新的異常類型 |
接下來,看一個(gè) demo
@Service @Slf4j @Getter publicclassRetryService3{ privateintcount=0; privateintretryCount=0; privateintfallbackCount=0; privateintrecoverCount=0; publicvoidclean(){ this.retryCount=0; this.fallbackCount=0; this.recoverCount=0; } /** *Command請(qǐng)求,啟動(dòng)重試機(jī)制 */ @Action(type=ActionType.COMMAND) @SmartFault(recover="recover") publicLongretry(Longinput)throwsThrowable{ this.retryCount++; returndoSomething(input); } /** *Query請(qǐng)求,啟動(dòng)Fallback機(jī)制 */ @Action(type=ActionType.QUERY) @SmartFault(recover="recover") publicLongfallback(Longinput)throwsThrowable{ this.fallbackCount++; returndoSomething(input); } @Recover publicLongrecover(Throwablee,Longinput){ this.recoverCount++; log.info("recover-{}",input); returninput; } privateLongdoSomething(Longinput){ //偶數(shù)拋出異常 if(count++%2==0){ log.info("Error-{}",input); thrownewRuntimeException(); } log.info("Success-{}",input); returninput; } }
測(cè)試代碼如下:
@SpringBootTest(classes=DemoApplication.class) publicclassRetryService3Test{ @Autowired privateRetryService3retryService; @BeforeEach publicvoidsetup(){ retryService.clean(); } @Test publicvoidretry()throwsThrowable{ for(inti=0;i100;?i++){ ????????????retryService.retry(i?+?0L); ????????} ????????Assertions.assertTrue(retryService.getRetryCount()?>0); Assertions.assertTrue(retryService.getRecoverCount()==0); Assertions.assertTrue(retryService.getFallbackCount()==0); } @Test publicvoidfallback()throwsThrowable{ for(inti=0;i100;?i++){ ????????????retryService.fallback(i?+?0L); ????????} ????????Assertions.assertTrue(retryService.getRetryCount()?==?0); ????????Assertions.assertTrue(retryService.getRecoverCount()?>0); Assertions.assertTrue(retryService.getFallbackCount()>0); } }
運(yùn)行 retry 測(cè)試,日志如下:
[main]c.g.l.c.f.smart.SmartFaultExecutor:actiontypeisCOMMAND [main]c.g.l.faultrecovery.smart.RetryService3:Error-0 [main]c.g.l.c.f.smart.SmartFaultExecutor:Retrymethodpublicjava.lang.Longcom.geekhalo.lego.faultrecovery.smart.RetryService3.retry(java.lang.Long)throwsjava.lang.Throwableuse[0] [main]c.g.l.faultrecovery.smart.RetryService3:Success-0
可見,當(dāng) action type 為 COMMAND 時(shí):
第一次調(diào)用時(shí),觸發(fā)異常,打印: Error-0
此時(shí) SmartFaultExecutor 主動(dòng)進(jìn)行重試,打印: Retry method xxxx
方法重試成功,RetryService3 打印: Success-0
方法主動(dòng)進(jìn)行重試,流程從異常中恢復(fù),處理過程和效果符合預(yù)期。
運(yùn)行 fallback 測(cè)試,日志如下:
[main]c.g.l.c.f.smart.SmartFaultExecutor:actiontypeisQUERY [main]c.g.l.faultrecovery.smart.RetryService3:Error-0 [main]c.g.l.c.f.smart.SmartFaultExecutor:recoverFromERRORformethodReflectiveMethodInvocation:publicjava.lang.Longcom.geekhalo.lego.faultrecovery.smart.RetryService3.fallback(java.lang.Long)throwsjava.lang.Throwable;targetisofclass[com.geekhalo.lego.faultrecovery.smart.RetryService3] [main]c.g.l.faultrecovery.smart.RetryService3:recover-0
可見,當(dāng) action type 為 QUERY 時(shí):
第一次調(diào)用時(shí),觸發(fā)異常,打印: Error-0
SmartFaultExecutor 執(zhí)行 Fallback 策略,打印:recover From ERROR for method xxxx
調(diào)用RetryService3的 recover 方法,獲取最終返回值。RetryService3 打印:recover-0
異常后自動(dòng)執(zhí)行 fallback,將流程從異常中恢復(fù)過來,處理過程和效果符合預(yù)期。
3. 設(shè)計(jì)&擴(kuò)展
3.1 核心設(shè)計(jì)
整體流程如下:
ActionAspect 從 @Action 中讀取配置信息,將請(qǐng)求類型綁定到線程上下文
然后執(zhí)行正常業(yè)務(wù)邏輯
當(dāng)調(diào)用 @SmartFault 注解的方法時(shí),會(huì)被 SmartFaultMethodInterceptor 攔截器攔截
攔截器通過 ActionTypeProvider 獲取當(dāng)前的 ActionType
根據(jù) ActionType 對(duì)請(qǐng)求進(jìn)行路由
如果是 COMMAND 操作,將使用 RetryTemplate 執(zhí)行請(qǐng)求,在發(fā)生異常時(shí),通過重試配置進(jìn)行請(qǐng)求重發(fā),從而最大限度的獲得遠(yuǎn)程結(jié)果
如果是 QUERY 操作,將使用 FallbackTemplate(重試次數(shù)為0的 RetryTemplate)執(zhí)行請(qǐng)求,當(dāng)發(fā)生異常時(shí),調(diào)用 fallback 方法,執(zhí)行配置的 recover 方法,直接使用返回結(jié)果
獲取遠(yuǎn)程結(jié)果后,執(zhí)行后續(xù)的業(yè)務(wù)邏輯
最后,ActionAspect 將 ActionType 從線程上下文中移除。
審核編輯:劉清
-
cpu
+關(guān)注
關(guān)注
68文章
11038瀏覽量
216030 -
SQL
+關(guān)注
關(guān)注
1文章
780瀏覽量
44814 -
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
3901瀏覽量
65783
原文標(biāo)題:容災(zāi)方案:Retry 和 Fallback 該怎么抉擇?
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
FPGA,該如何抉擇呢?
為什么activate_retry_interval的值不等于3?
Uboot更新固件報(bào)錯(cuò)ARP retry count exceeded如何解決?
本本發(fā)熱導(dǎo)致“發(fā)燒” 那該怎么辦呢?
榮耀9、小米6對(duì)比評(píng)測(cè):這對(duì)難兄難弟,外觀、價(jià)格也都差不多該如何抉擇
華為P20Pro和iPhone 8Plus該如何抉擇
地鐵二維碼改造是如何實(shí)現(xiàn)的呢?傳統(tǒng)地鐵閘機(jī)廠商又該如何抉擇呢?
中國移動(dòng)攜手華為打通了首個(gè)5G EPS Fallback語音視頻通話
四案例EPS Fallback問題解決資料下載

SA EPS FallBack重要信令節(jié)點(diǎn)資料下載

MOS管該如何抉擇
縮短MultiBoot流程中的回跳 (Fallback)時(shí)間
5G EPS Fallback語音方案流程總結(jié)
如何抉擇PLC和DCS系統(tǒng)

評(píng)論