應用中須要實現一個功能: 須要將數據上傳到遠程存儲服務,同時在返回處理成功狀況下作其餘操做。這個功能不復雜,分爲兩個步驟:第一步調用遠程的Rest服務邏輯包裝給處理方法返回處理結果;第二步拿到第一步結果或者捕捉異常,若是出現錯誤或異常實現重試上傳邏輯,不然繼續邏輯操做。java
這個問題的技術點在於可以觸發重試,以及重試狀況下邏輯有效執行。程序員
包裝正常上傳邏輯基礎上,經過判斷返回結果或監聽異常決策是否重試,同時爲了解決當即重試的無效執行(假設異常是有外部執行不穩定致使的),休眠必定延遲時間從新執行功能邏輯。算法
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);//一次重試
}
}
複製代碼
上述方案仍是有可能重試無效,解決這個問題嘗試增長重試次數retrycount以及重試間隔週期interval,達到增長重試有效的可能性。spring
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);//延遲屢次重試
}
}
複製代碼
方案一和方案二存在一個問題:正常邏輯和重試邏輯強耦合,重試邏輯很是依賴正常邏輯的執行結果,對正常邏輯預期結果被動重試觸發,對於重試根源每每因爲邏輯複雜被淹沒,可能致使後續運維對於重試邏輯要解決什麼問題產生不一致理解。重試正確性難保證並且不利於運維,緣由是重試設計依賴正常邏輯異常或重試根源的臆測。設計模式
那有沒有能夠參考的方案實現正常邏輯和重試邏輯解耦,同時可以讓重試邏輯有一個標準化的解決思路?答案是有:那就是基於代理設計模式的重試工具,咱們嘗試使用相應工具來重構上述場景。安全
命令設計模式具體定義不展開闡述,主要該方案看中命令模式可以經過執行對象完成接口操做邏輯,同時內部封裝處理重試邏輯,不暴露實現細節,對於調用者來看就是執行了正常邏輯,達到解耦的目標,具體看下功能實現。(類圖結構)bash
IRetry約定了上傳和重試接口,其實現類OdpsRetry封裝ODPS上傳邏輯,同時封裝重試機制和重試策略。與此同時使用recover方法在結束執行作恢復操做。服務器
而咱們的調用者LogicClient無需關注重試,經過重試者Retryer實現約定接口功能,同時 Retryer須要對重試邏輯作出響應和處理, Retryer具體重試處理又交給真正的IRtry接口的實現類OdpsRetry完成。經過採用命令模式,優雅實現正常邏輯和重試邏輯分離,同時經過構建重試者角色,實現正常邏輯和重試邏輯的分離,讓重試有更好的擴展性。併發
spring-retry是一個開源工具包,目前可用的版本爲1.1.2.RELEASE,該工具把重試操做模板定製化,能夠設置重試策略和回退策略。同時重試執行實例保證線程安全,具體場景操做實例以下:app
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對於「重試」的抽象。
while(someCondition()) {
try{
doSth();
break;
} catch(Throwable th) {
modifyCondition();
wait();
}
}
if(stillFail) {
doSthWhenStillFail();
}複製代碼
同步重試代碼基本能夠表示爲上述,可是Spring Retry對其進行了很是優雅地抽象,雖然主要邏輯不變,可是看起來倒是舒服多了。主要的接口抽象以下圖所示:
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方法執行操做邏輯,同時封裝重試源操做。
https://blog.csdn.net/paul_wei2008/article/details/53871442