新聞中心
1. 概覽
在分布式場景中,Retry 和 Fallback 是最常見的容災方案。

創(chuàng)新互聯(lián)建站長期為1000+客戶提供的網(wǎng)站建設(shè)服務(wù),團隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為望奎企業(yè)提供專業(yè)的成都網(wǎng)站制作、做網(wǎng)站,望奎網(wǎng)站改版等技術(shù)服務(wù)。擁有十余年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
- Retry 就是在調(diào)用遠程接口失敗時,Client 主動發(fā)起重試請求,以期待獲得最終結(jié)果,從而完成整個流程
- Fallback 是在調(diào)用遠程接口失敗時,Client 不進行重試而是調(diào)用一個特殊的 fallback 方法,從這個方法中獲取結(jié)果,使流程能夠繼續(xù)下去
那 Retry 和 Fallback 該怎么抉擇呢?
1.1. 背景
首先,先看下 Retry 和 Fallback 都是怎么幫助流程進行自我恢復的。
1.1.1. Retry
現(xiàn)在有一個生產(chǎn)流程:
核心流程如下:
- 從商品服務(wù)中獲取商品信息
- 根據(jù)商品信息創(chuàng)建訂單
- 將訂單保存到數(shù)據(jù)庫
如果發(fā)生網(wǎng)絡(luò)抖動,將導致生產(chǎn)失敗。
- 在調(diào)用商品服務(wù)獲取商品時,由于網(wǎng)絡(luò)異常,接口調(diào)用失敗
- 由于無法獲取商品信息,生產(chǎn)流程被異常中斷
由于上產(chǎn)流程太過重要,系統(tǒng)需盡最大努力保障用戶能夠完成下單操作,那針對網(wǎng)絡(luò)抖動這個問題,可以通過 Retry 進行修復。
image
- 在第一次獲取商品信息時,由于網(wǎng)絡(luò)問題導致獲取失敗
- 系統(tǒng)不會直接拋出異常,而是在等待一段時間后,重新發(fā)起第二次請求,也就是 Retry 操作
- 網(wǎng)絡(luò)恢復,第二次請求成功獲取商品信息
- 流程繼續(xù)運行,最終完成用戶生產(chǎn)
Retry 機制非常適合服務(wù)短時間不可用,或某個服務(wù)節(jié)點異常 這類場景。
1.1.2. Fallback
一個生產(chǎn)驗證接口,主流程如下:
- 調(diào)用商品服務(wù)的接口獲取商品信息
- 根據(jù)商品和用戶信息判斷用戶是否能夠購買該商品
同樣,假設(shè)在訪問商品服務(wù)時出現(xiàn)網(wǎng)絡(luò)異常:
由于無法獲取商品信息,從而導致整個驗證流程被異常中斷,用戶操作被迫終止。
聰明的你估計會說那就使用 Retry 呀,是的:
如果是短時不可用,通過 Retry 機制便可以恢復流程。
但,如果是商品服務(wù)壓力過大,響應時間過長呢?比如,商品服務(wù)流量激增,導致 DB CPU 飆升,出現(xiàn)大量的慢 SQL,這時觸發(fā)了系統(tǒng)的 Retry 會是怎樣?
image
- 在獲取商品失敗后,系統(tǒng)自動觸發(fā) Retry 機制
- 由于是商品服務(wù)本身出了問題,第二次請求仍舊失敗
- 服務(wù)又觸發(fā)了第三次請求,仍未獲取結(jié)果
- 達到最大重試次數(shù),仍舊無法獲取商品,只能通過異常中斷用戶請求
通過 Retry 機制未能將流程從異常中恢復過來,也給下游的 商品服務(wù) 造成了巨大傷害。
- 商品服務(wù)壓力大,響應時間長
- 上游系統(tǒng)由于超時觸發(fā)自動重試
- 自動重試增大了對商品服務(wù)的調(diào)用
- 商品服務(wù)請求量更大,更難以從故障中恢復
這就是常說的“讀放大”,假設(shè)用戶驗證是否能夠購買請求的請求量為 n,那極端情況下 商品服務(wù)的請求量為 3n (其中 2n 是由 Retry 機制造成)
此時,Retry 就不是一個好的方案。我們先退回業(yè)務(wù)場景進行思考,如果無法獲取商品,驗證接口是否可以直接放行,先讓用戶完成購買?
如果,這個業(yè)務(wù)假設(shè)能夠接受的話,那就到了 Fallback 上場的時候了。
- 調(diào)用商品服務(wù)獲取商品信息失敗
- 系統(tǒng)不會進行重試,而是觸發(fā) fallback 機制
- fallback 會調(diào)用指定的一個方法,并將返回值作為遠程接口的返回值
- 接下來的流程使用 fallback 方法的返回值完成業(yè)務(wù)邏輯
1.1.3. 場景思考
同樣是對商品服務(wù)接口(同一個接口)的調(diào)用,在不同的場景需要使用不同的策略用以恢復業(yè)務(wù)流程,通常情況下:
- Command 場景優(yōu)先使用 Retry
- 這種流量極為重要,最好能保障流程的完整性
- 通常寫流量比較小,小范圍 Retry 不會對下游系統(tǒng)造成巨大影響
- Query 場景優(yōu)選使用 Fallabck
- 大多數(shù)展示場景,哪怕部分信息沒有獲取到對整體的影響也比較小
- 通常讀場景流量較高,Retry 對下游系統(tǒng)的傷害不容忽視
那面對一個遠程接口被多個場景使用,我們該怎么處理呢?
- 提供兩組接口,一個具有 Retry 能力,一個具有 Fallback 能力,由使用方根據(jù)業(yè)務(wù)場景進行選擇?
- 還是…
1.2. 目標
- 遠程接口具備 Retry 和 Fallback 能力
- 能夠根據(jù)上下文不同場景,在發(fā)生調(diào)用異常時動態(tài)選擇 Retry 或 Fallback 進行流程恢復
2. 快速入門
2.1. 準備環(huán)境
項目主要依賴 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
public class RetryConfiguration {
}
2.2. 構(gòu)建 ActionTypeProvider
在完成基本配置后,需要準備一個 ActionTypeProvider 用以提供上下文信息。
ActionTypeProvider 接口定義如下:
public interface ActionTypeProvider {
ActionType get();
}
public enum ActionType {
COMMAND, QUERY
}
通常情況下,我們會使用 ThreadLocal 組件將 ActionType 存儲于線程上下文,在使用時從上下中獲取相關(guān)信息。
public class ActionContext {
private static final ThreadLocal ACTION_TYPE_THREAD_LOCAL = new ThreadLocal<>();
public static void set(ActionType actionType){
ACTION_TYPE_THREAD_LOCAL.set(actionType);
}
public static ActionType get(){
return ACTION_TYPE_THREAD_LOCAL.get();
}
public static void clear(){
ACTION_TYPE_THREAD_LOCAL.remove();
}
}
有了上下文之后,
ActionBasedActionTypeProvider 直接從 Context 中獲取 ActionType 具體如下
@Component
public class ActionBasedActionTypeProvider implements ActionTypeProvider {
@Override
public ActionType get(){
return ActionContext.get();
}
}
上下文中的 ActionType 又是怎么進行管理的呢,包括信息綁定和信息清理?
最常用的方式便是:
- 提供一個注解,在方法上添加注解用于對 ActionType 的配置;
- 提供一個攔截器,對方法調(diào)用進行攔截。方法調(diào)用前,從注解中獲取配置信息并綁定到上下文;方法調(diào)用后,主動清理上下文信息;
核心實現(xiàn)為:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
ActionType type();
}
@Aspect
@Component
@Order(Integer.MIN_VALUE)
public class ActionAspect {
@Pointcut("@annotation(com.geekhalo.lego.faultrecovery.smart.Action)")
public void pointcut() {
}
@Around(value = "pointcut()")
public Object action(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Action annotation = methodSignature.getMethod().getAnnotation(Action.class);
ActionContext.set(annotation.type());
try {
return joinPoint.proceed();
}finally {
ActionContext.clear();
}
}
}
在這些組件的幫助下,我們只需在方法上基于 @Action 注解進行標記,便能夠?qū)?ActionType 綁定到上下文。
2.3. 使用 @SmartFault
在將 ActionType 綁定到上下文之后,接下來要做的便是對 遠程接口 進行配置。遠程接口的配置工作主要由 @SmartFault 來完成。
其核心配置項包括:
|
配置項 |
含義 |
默認配置 |
|
recover |
fallback 方法名稱 | |
|
maxRetry |
最大重試次數(shù) |
3 |
|
include |
觸發(fā)重試的異常類型 | |
|
exclude |
不需要重新的異常類型 |
接下來,看一個 demo
@Service
@Slf4j
@Getter
public class RetryService3 {
private int count = 0;
private int retryCount = 0;
private int fallbackCount = 0;
private int recoverCount = 0;
public void clean(){
this.retryCount = 0;
this.fallbackCount = 0;
this.recoverCount = 0;
}
/**
* Command 請求,啟動重試機制
*/
@Action(type = ActionType.COMMAND)
@SmartFault(recover = "recover")
public Long retry(Long input) throws Throwable{
this.retryCount ++;
return doSomething(input);
}
/**
* Query 請求,啟動Fallback機制
*/
@Action(type = ActionType.QUERY)
@SmartFault(recover = "recover")
public Long fallback(Long input) throws Throwable{
this.fallbackCount ++;
return doSomething(input);
}
@Recover
public Long recover(Throwable e, Long input){
this.recoverCount ++;
log.info("recover-{}", input);
return input;
}
private Long doSomething(Long input) {
// 偶數(shù)拋出異常
if (count ++ % 2 == 0){
log.info("Error-{}", input);
throw new RuntimeException();
}
log.info("Success-{}", input);
return input;
}
}
測試代碼如下:
@SpringBootTest(classes = DemoApplication.class)
public class RetryService3Test {
@Autowired
private RetryService3 retryService;
@BeforeEach
public void setup(){
retryService.clean();
}
@Test
public void retry() throws Throwable{
for (int i = 0; i < 100; i++){
retryService.retry(i + 0L);
}
Assertions.assertTrue(retryService.getRetryCount() > 0);
Assertions.assertTrue(retryService.getRecoverCount() == 0);
Assertions.assertTrue(retryService.getFallbackCount() == 0);
}
@Test
public void fallback() throws Throwable{
for (int i = 0; i < 100; i++){
retryService.fallback(i + 0L);
}
Assertions.assertTrue(retryService.getRetryCount() == 0);
Assertions.assertTrue(retryService.getRecoverCount() > 0);
Assertions.assertTrue(retryService.getFallbackCount() > 0);
}
}
運行 retry 測試,日志如下:
[main] c.g.l.c.f.smart.SmartFaultExecutor : action type is COMMAND
[main] c.g.l.faultrecovery.smart.RetryService3 : Error-0
[main] c.g.l.c.f.smart.SmartFaultExecutor : Retry method public java.lang.Long com.geekhalo.lego.faultrecovery.smart.RetryService3.retry(java.lang.Long) throws java.lang.Throwable use [0]
[main] c.g.l.faultrecovery.smart.RetryService3 : Success-0
可見,當 action type 為 COMMAND 時:
- 第一次調(diào)用時,觸發(fā)異常,打?。?Error-0
- 此時 SmartFaultExecutor 主動進行重試,打?。?Retry method xxxx
- 方法重試成功,RetryService3 打?。?Success-0
方法主動進行重試,流程從異常中恢復,處理過程和效果符合預期。
運行 fallback 測試,日志如下:
[main] c.g.l.c.f.smart.SmartFaultExecutor : action type is QUERY
[main] c.g.l.faultrecovery.smart.RetryService3 : Error-0
[main] c.g.l.c.f.smart.SmartFaultExecutor : recover From ERROR for method ReflectiveMethodInvocation: public java.lang.Long com.geekhalo.lego.faultrecovery.smart.RetryService3.fallback(java.lang.Long) throws java.lang.Throwable; target is of class [com.geekhalo.lego.faultrecovery.smart.RetryService3]
[main] c.g.l.faultrecovery.smart.RetryService3 : recover-0
可見,當 action type 為 QUERY 時:
- 第一次調(diào)用時,觸發(fā)異常,打?。?Error-0
- SmartFaultExecutor 執(zhí)行 Fallback 策略,打?。簉ecover From ERROR for method xxxx
- 調(diào)用RetryService3的 recover 方法,獲取最終返回值。RetryService3 打?。簉ecover-0
異常后自動執(zhí)行 fallback,將流程從異常中恢復過來,處理過程和效果符合預期。
3. 設(shè)計&擴展
3.1 核心設(shè)計
image
整體流程如下:
- ActionAspect 從 @Action 中讀取配置信息,將請求類型綁定到線程上下文
- 然后執(zhí)行正常業(yè)務(wù)邏輯
- 當調(diào)用 @SmartFault 注解的方法時,會被 SmartFaultMethodInterceptor 攔截器攔截
- 攔截器通過 ActionTypeProvider 獲取當前的 ActionType
- 根據(jù) ActionType 對請求進行路由
- 如果是 COMMAND 操作,將使用 RetryTemplate 執(zhí)行請求,在發(fā)生異常時,通過重試配置進行請求重發(fā),從而最大限度的獲得遠程結(jié)果
- 如果是 QUERY 操作,將使用 FallbackTemplate(重試次數(shù)為0的 RetryTemplate)執(zhí)行請求,當發(fā)生異常時,調(diào)用 fallback 方法,執(zhí)行配置的 recover 方法,直接使用返回結(jié)果
- 獲取遠程結(jié)果后,執(zhí)行后續(xù)的業(yè)務(wù)邏輯
- 最后,ActionAspect 將 ActionType 從線程上下文中移除
4. 項目信息
項目倉庫地址:https://gitee.com/litao851025/lego
項目文檔地址:https://gitee.com/litao851025/lego/wikis/support/smart-fault
網(wǎng)站名稱:Retry&Fallback是利器還是魔咒?
轉(zhuǎn)載來源:http://fisionsoft.com.cn/article/djoesgc.html


咨詢
建站咨詢
