業務場景
應用中需要實現一個功能: 需要將資料上傳到遠端儲存服務,同時在返回處理成功情況下做其他操作。這個功能不復雜,分為兩個步驟:第一步呼叫遠端的Rest服務邏輯包裝給處理方法返回處理結果;第二步拿到第一步結果或者捕捉異常,如果出現錯誤或異常實現重試上傳邏輯,否則繼續邏輯操作。
解決方案演化
這個問題的技術點在於能夠觸發重試,以及重試情況下邏輯有效執行。
解決方案一:try-catch-redo簡單重試模式
包裝正常上傳邏輯基礎上,通過判斷返回結果或監聽異常決策是否重試,同時為了解決立即重試的無效執行(假設異常是有外部執行不穩定導致的),休眠一定延遲時間重新執行功能邏輯。
public void commonRetry(Map<String, Object> dataMap) throws InterruptedException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
Thread.sleep(1000);
uploadToOdps(paramMap); //一次重試
}
} catch (Exception e) {
Thread.sleep(1000);
uploadToOdps(paramMap);//一次重試
}
}
複製程式碼
解決方案二:try-catch-redo-retry strategy策略重試模式
上述方案還是有可能重試無效,解決這個問題嘗試增加重試次數retrycount以及重試間隔週期interval,達到增加重試有效的可能性。
public void commonRetry(Map<String, Object> dataMap) throws InterruptedException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("tableName", "creativeTable");
paramMap.put("ds", "20160220");
paramMap.put("dataMap", dataMap);
boolean result = false;
try {
result = uploadToOdps(paramMap);
if (!result) {
reuploadToOdps(paramMap,1000L,10);//延遲多次重試
}
} catch (Exception e) {
reuploadToOdps(paramMap,1000L,10);//延遲多次重試
}
}
複製程式碼
方案一和方案二存在一個問題:正常邏輯和重試邏輯強耦合,重試邏輯非常依賴正常邏輯的執行結果,對正常邏輯預期結果被動重試觸發,對於重試根源往往由於邏輯複雜被淹沒,可能導致後續運維對於重試邏輯要解決什麼問題產生不一致理解。重試正確性難保證而且不利於運維,原因是重試設計依賴正常邏輯異常或重試根源的臆測。
優雅重試方案嘗試
那有沒有可以參考的方案實現正常邏輯和重試邏輯解耦,同時能夠讓重試邏輯有一個標準化的解決思路?答案是有:那就是基於代理設計模式的重試工具,我們嘗試使用相應工具來重構上述場景。
嘗試方案一:應用命令設計模式解耦正常和重試邏輯
命令設計模式具體定義不展開闡述,主要該方案看中命令模式能夠通過執行物件完成介面操作邏輯,同時內部封裝處理重試邏輯,不暴露實現細節,對於呼叫者來看就是執行了正常邏輯,達到解耦的目標,具體看下功能實現。(類圖結構)
IRetry約定了上傳和重試介面,其實現類OdpsRetry封裝ODPS上傳邏輯,同時封裝重試機制和重試策略。與此同時使用recover方法在結束執行做恢復操作。
而我們的呼叫者LogicClient無需關注重試,通過重試者Retryer實現約定介面功能,同時 Retryer需要對重試邏輯做出響應和處理, Retryer具體重試處理又交給真正的IRtry介面的實現類OdpsRetry完成。通過採用命令模式,優雅實現正常邏輯和重試邏輯分離,同時通過構建重試者角色,實現正常邏輯和重試邏輯的分離,讓重試有更好的擴充套件性。
嘗試方案二:spring-retry 規範正常和重試邏輯
spring-retry是一個開源工具包,目前可用的版本為1.1.2.RELEASE,該工具把重試操作模板定製化,可以設定重試策略和回退策略。同時重試執行例項保證執行緒安全,具體場景操作例項如下:
public void upload(final Map<String, Object> map) throws Exception {
// 構建重試模板例項
RetryTemplate retryTemplate = new RetryTemplate();
// 設定重試策略,主要設定重試次數
SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
// 設定重試回退操作策略,主要設定重試間隔時間
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(100);
retryTemplate.setRetryPolicy(policy);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
// 通過RetryCallback 重試回撥例項包裝正常邏輯邏輯,第一次執行和重試執行執行的都是這段邏輯
final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {
//RetryContext 重試操作上下文約定,統一spring-try包裝
public Object doWithRetry(RetryContext context) throws Exception {
System.out.println("do some thing");
Exception e = uploadToOdps(map);
System.out.println(context.getRetryCount());
throw e;//這個點特別注意,重試的根源通過Exception返回
}
};
// 通過RecoveryCallback 重試流程正常結束或者達到重試上限後的退出恢復操作例項
final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {
public Object recover(RetryContext context) throws Exception {
System.out.println("do recory operation");
return null;
}
};
try {
// 由retryTemplate 執行execute方法開始邏輯執行
retryTemplate.execute(retryCallback, recoveryCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
簡單剖析下案例程式碼,RetryTemplate 承擔了重試執行者的角色,它可以設定SimpleRetryPolicy(重試策略,設定重試上限,重試的根源實體),FixedBackOffPolicy(固定的回退策略,設定執行重試回退的時間間隔)。 RetryTemplate通過execute提交執行操作,需要準備RetryCallback 和RecoveryCallback 兩個類例項,前者對應的就是重試回撥邏輯例項,包裝正常的功能操作,RecoveryCallback實現的是整個執行操作結束的恢復操作例項。
RetryTemplate的execute 是執行緒安全的,實現邏輯使用ThreadLocal儲存每個執行例項的RetryContext執行上下文。
Spring-retry工具雖能優雅實現重試,但是存在兩個不友好設計:一個是 重試實體限定為Throwable子類,說明重試針對的是可捕捉的功能異常為設計前提的,但是我們希望依賴某個資料物件實體作為重試實體,但Sping-retry框架必須強制轉換為Throwable子類。另一個就是重試根源的斷言物件使用的是doWithRetry的Exception 異常例項,不符合正常內部斷言的返回設計。
Spring Retry提倡以註解的方式對方法進行重試,重試邏輯是同步執行的,重試的“失敗”針對的是Throwable,如果你要以返回值的某個狀態來判定是否需要重試,可能只能通過自己判斷返回值然後顯式丟擲異常了。
Spring 對於Retry的抽象
“抽象”是每個程式設計師必備的素質。對於資質平平的我來說,沒有比模仿與理解優秀原始碼更好地進步途徑了吧。為此,我將其核心邏輯重寫了一遍…下面就看看Spring Retry對於“重試”的抽象。
“重試”邏輯
while(someCondition()) {
try{
doSth();
break;
} catch(Throwable th) {
modifyCondition();
wait();
}
}
if(stillFail) {
doSthWhenStillFail();
}複製程式碼
同步重試程式碼基本可以表示為上述,但是Spring Retry對其進行了非常優雅地抽象,雖然主要邏輯不變,但是看起來卻是舒服多了。主要的介面抽象如下圖所示:
- RetryCallback: 封裝你需要重試的業務邏輯(上文中的doSth)
- RecoverCallback:封裝在多次重試都失敗後你需要執行的業務邏輯(上文中的doSthWhenStillFail)
- RetryContext: 重試語境下的上下文,可用於在多次Retry或者Retry 和Recover之間傳遞引數或狀態(在多次doSth或者doSth與doSthWhenStillFail之間傳遞引數)
- RetryOperations : 定義了“重試”的基本框架(模板),要求傳入RetryCallback,可選傳入RecoveryCallback;
- RetryListener:典型的“監聽者”,在重試的不同階段通知“監聽者”(例如doSth,wait等階段時通知)
- RetryPolicy : 重試的策略或條件,可以簡單的進行多次重試,可以是指定超時時間進行重試(上文中的someCondition)
- BackOffPolicy: 重試的回退策略,在業務邏輯執行發生異常時。如果需要重試,我們可能需要等一段時間(可能伺服器過於繁忙,如果一直不間隔重試可能拖垮伺服器),當然這段時間可以是0,也可以是固定的,可以是隨機的(參見tcp的擁塞控制演算法中的回退策略)。回退策略在上文中體現為wait();
- RetryTemplate :RetryOperations的具體實現,組合了RetryListener[],BackOffPolicy,RetryPolicy。
嘗試方案三:guava-retryer 分離正常和重試邏輯
Guava retryer工具與spring-retry類似,都是通過定義重試者角色來包裝正常邏輯重試,但是Guava retryer有更優的策略定義,在支援重試次數和重試頻度控制基礎上,能夠相容支援多個異常或者自定義實體物件的重試源定義,讓重試功能有更多的靈活性。Guava Retryer也是執行緒安全的,入口呼叫邏輯採用的是Java.util.concurrent.Callable的call方法,示例程式碼如下:
public void uploadOdps(final Map<String, Object> map) {
// RetryerBuilder 構建重試例項 retryer,可以設定重試源且可以支援多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔
Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
.retryIfException().//設定異常重試源
retryIfResult(new Predicate<Boolean>() {//設定自定義段元重試源,
@Override
public boolean apply(Boolean state) {//特別注意:這個apply返回true說明需要重試,與操作邏輯的語義要區分
return true;
}
})
.withStopStrategy(StopStrategies.stopAfterAttempt(5))//設定重試5次,同樣可以設定重試超時時間
.withWaitStrategy(WaitStrategies.fixedWait(100L, TimeUnit.MILLISECONDS)).build();//設定每次重試間隔
try {
//重試入口採用call方法,用的是java.util.concurrent.Callable<V>的call方法,所以執行是執行緒安全的
boolean result = retryer.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
try {
//特別注意:返回false說明無需重試,返回true說明需要繼續重試
return uploadToOdps(map);
} catch (Exception e) {
throw new Exception(e);
}
}
});
} catch (ExecutionException e) {
} catch (RetryException ex) {
}
}
複製程式碼
示例程式碼原理分析:
RetryerBuilder是一個factory建立者,可以定製設定重試源且可以支援多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔,建立重試者Retryer例項。
RetryerBuilder的重試源支援Exception異常物件 和自定義斷言物件,通過retryIfException 和retryIfResult設定,同時支援多個且能相容。
RetryerBuilder的等待時間和重試限制配置採用不同的策略類實現,同時對於等待時間特徵可以支援無間隔和固定間隔方式。
Retryer 是重試者例項,通過call方法執行操作邏輯,同時封裝重試源操作。
優雅重試共性和原理
- 正常和重試優雅解耦,重試斷言條件例項或邏輯異常例項是兩者溝通的媒介。
- 約定重試間隔,差異性重試策略,設定重試超時時間,進一步保證重試有效性以及重試流程穩定性。
- 都使用了命令設計模式,通過委託重試物件完成相應的邏輯操作,同時內部封裝實現重試邏輯。
- Spring-tryer和guava-tryer工具都是執行緒安全的重試,能夠支援併發業務場景的重試邏輯正確性。
優雅重試適用場景
- 功能邏輯中存在不穩定依賴場景,需要使用重試獲取預期結果或者嘗試重新執行邏輯不立即結束。比如遠端介面訪問,資料載入訪問,資料上傳校驗等等。
- 對於異常場景存在需要重試場景,同時希望把正常邏輯和重試邏輯解耦。
- 對於需要基於資料媒介互動,希望通過重試輪詢檢測執行邏輯場景也可以考慮重試方案。
參考資料
https://blog.csdn.net/paul_wei2008/article/details/53871442