應用中需要實現(xiàn)一個功能唤殴,需要將數(shù)據(jù)上傳到遠程存儲服務斋攀,同時在返回成功情況下做其他操作。這個功能不復雜弧哎,分為兩個步驟雁比,第一步調(diào)用遠程rest服務上傳數(shù)據(jù)后對返回的結(jié)果進行處理;第二步拿到第一步結(jié)果或者異常撤嫩,如果出現(xiàn)錯誤或者異常則實現(xiàn)重試上傳邏輯偎捎,否則繼續(xù)接下來的業(yè)務操作。
常規(guī)解決方案
try-catch-redo簡單重試模式
在包裝正常上傳邏輯的基礎(chǔ)上,通過判斷返回結(jié)果或監(jiān)聽異常決定是否需要重試茴她,同時為了解決立即重試的無效性(假設異常是由外部不穩(wěn)定導致的:網(wǎng)絡抖動)寻拂,休眠一定延遲時間后重新執(zhí)行該邏輯功能。
private void commonRetry() throws InterruptedException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("tableName", "cretiveTable");
paramMap.put("ds", "20160909");
paramMap.put("dataMap", "data");
paramMap.put("tableName", "cretiveTable");
boolean result = false;
try {
result = uploadOdps(paramMap);
if (!result) {
Thread.sleep(1000);
uploadOdps(paramMap);//單次重試
}
} catch (Exception e) {
Thread.sleep(1000);
uploadOdps(paramMap);//單次重試
}
}
try-catch-redo-retry strategy策略重試模式
上述方案還是有可能重試無效丈牢,解決這個問題嘗試增加重試次數(shù)retrycount以及重試間隔周期interval祭钉,達到增加重試有效的可能性。
private void commonRetry() throws InterruptedException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("tableName", "cretiveTable");
paramMap.put("ds", "20160909");
paramMap.put("dataMap", "data");
paramMap.put("tableName", "cretiveTable");
boolean result = false;
try {
result = uploadOdps(paramMap);
if (!result) {
Thread.sleep(1000);
reUploadOdps(paramMap, 1000L, 3);//延遲多次重試
}
} catch (Exception e) {
Thread.sleep(1000);
reUploadOdps(paramMap, 1000L, 3);//延遲多次重試
}
}
方案一和方案二存在一個問題:正常邏輯和重試邏輯強耦合己沛,重試邏輯非常依賴正常邏輯的執(zhí)行結(jié)果慌核,對正常邏輯預期結(jié)果被動重試觸發(fā),對于重試根源往往由于邏輯復雜被淹沒申尼,可能導致后續(xù)運維對于重試邏輯要解決什么問題產(chǎn)生不一致理解垮卓。重試正確性難保證而且不利于運維,原因是重試設計依賴正常邏輯異尘фⅲ或重試根源的臆測扒接。
優(yōu)雅重試方案嘗試
應用命令設計模式解耦正常和重試邏輯
命令設計模式具體定義不展開闡述伪货,主要該方案看中命令模式能夠通過執(zhí)行對象完成接口操作邏輯们衙,同時內(nèi)部封裝處理重試邏輯,不暴露實現(xiàn)細節(jié)碱呼,對于調(diào)用者來看就是執(zhí)行了正常邏輯蒙挑,達到解耦的目標,具體看下功能實現(xiàn)愚臀。(類圖結(jié)構(gòu))
[圖片上傳失敗...(image-40ca90-1558507124456)]
而我們的調(diào)用者LogicClient無需關(guān)注重試忆蚀,通過重試者Retryer實現(xiàn)約定接口功能,同時 Retryer需要對重試邏輯做出響應和處理姑裂, Retryer具體重試處理又交給真正的IRtry接口的實現(xiàn)類OdpsRetry完成馋袜。通過采用命令模式,優(yōu)雅實現(xiàn)正常邏輯和重試邏輯分離舶斧,同時通過構(gòu)建重試者角色欣鳖,實現(xiàn)正常邏輯和重試邏輯的分離,讓重試有更好的擴展性茴厉。
使用Guava retryer優(yōu)雅的實現(xiàn)接口重調(diào)機制
Guava retryer工具與spring-retry類似泽台,都是通過定義重試者角色來包裝正常邏輯重試,但是Guava retryer有更優(yōu)的策略定義矾缓,在支持重試次數(shù)和重試頻度控制基礎(chǔ)上怀酷,能夠兼容支持多個異常或者自定義實體對象的重試源定義嗜闻,讓重試功能有更多的靈活性蜕依。Guava Retryer也是線程安全的,入口調(diào)用邏輯采用的是Java.util.concurrent.Callable的call方法。 使用Guava retryer 很簡單样眠,我們只要做以下幾步:
1竞滓、Maven POM 引入
<guava-retry.version>2.0.0</guava-retry.version>
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>${guava-retry.version}</version>
</dependency>
2、定義實現(xiàn)Callable接口的方法吹缔,以便Guava retryer能夠調(diào)用
private static Callable<Boolean> upload = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
String url = "test";
String result = HttpMethod.POST(url, new ArrayList<BasicNameValuePair>());
if (StringUtils.isEmpty(result)) {
throw new RuntimeException("result is blank");
}
final JSONObject json = JSON.parseObject(result);
if (json.getBoolean("result")) {
return true;
}
return false;
}
};
3商佑、定義Retry對象并設置相關(guān)策略
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
//拋出runtime異常、checked異常時都會重試厢塘,但是拋出error不會重試茶没。
.retryIfException()
//返回false也需要重試
.retryIfResult(Predicates.equalTo(false))
//重調(diào)策略
.withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
//嘗試次數(shù)
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(updateReimAgentsCall());
# 以下方式可以不用實現(xiàn)第二步中所說的實現(xiàn)Callable接口定義方法
//retry.call(() -> { FileUtils.downloadAttachment(projectNo, url, saveDir, fileName); return true; });
} catch (ExecutionException e) {
e.printStackTrace();
} catch (RetryException e) {
logger.error("xxx");
}
簡單三步就能使用Guava Retryer優(yōu)雅的實現(xiàn)重調(diào)方法。
RetryerBuilder是一個Factory創(chuàng)建者晚碾,可以自定義設置重試源且支持多個重試源抓半,可以配置重試次數(shù)或重試超時時間,以及可以配置等待時間間隔格嘁,創(chuàng)建重試者Retryer實例笛求。 RetryerBuilder的重試源支持Exception異常對象和自定義斷言對象,通過retryIfException
和retryIfResult
設置糕簿,同時支持多個且能兼容探入。
- retryIfException:拋出runtime異常、checked異常時都會重試懂诗,但是拋出error不會重試蜂嗽。
- retryIfRuntimeException:只會在拋runtime異常的時候才重試,checked異常和error都不重試殃恒。
- retryIfExceptionOfType:允許我們只在發(fā)生特定異常的時候才重試植旧,比如NullPointerException和IllegalStateException都屬于runtime異常,也包括自定義的error ?如:
# 只在拋出error重試
retryIfExceptionOfType(Error.class)
# 只有出現(xiàn)指定的異常的時候才重試离唐,如:  
retryIfExceptionOfType(IllegalStateException.class)
retryIfExceptionOfType(NullPointerException.class)
# 或者通過Predicate實現(xiàn)
retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),
Predicates.instanceOf(IllegalStateException.class)))
retryIfResult可以指定你的Callable方法在返回值的時候進行重試病附,如
// 返回false重試
retryIfResult(Predicates.equalTo(false))
//以_error結(jié)尾才重試
retryIfResult(Predicates.containsPattern("_error$"))
當發(fā)生重試之后,假如我們需要做一些額外的處理動作亥鬓,比如發(fā)個告警郵件啥的完沪,那么可以使用RetryListener
。每次重試之后贮竟,guava-retrying會自動回調(diào)我們注冊的監(jiān)聽丽焊。也可以注冊多個RetryListener,會按照注冊順序依次調(diào)用咕别。
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import java.util.concurrent.ExecutionException;
public class MyRetryListener<Boolean> implements RetryListener {
@Override
public <Boolean> void onRetry(Attempt<Boolean> attempt) {
// 第幾次重試,(注意:第一次重試其實是第一次調(diào)用)
System.out.print("[retry]time=" + attempt.getAttemptNumber());
// 距離第一次重試的延遲
System.out.print(",delay=" + attempt.getDelaySinceFirstAttempt());
// 重試結(jié)果: 是異常終止, 還是正常返回
System.out.print(",hasException=" + attempt.hasException());
System.out.print(",hasResult=" + attempt.hasResult());
// 是什么原因?qū)е庐惓?
if (attempt.hasException()) {
System.out.print(",causeBy=" + attempt.getExceptionCause().toString());
} else {
// 正常返回時的結(jié)果
System.out.print(",result=" + attempt.getResult());
}
// bad practice: 增加了額外的異常處理代碼
try {
Boolean result = attempt.get();
System.out.print(",rude get=" + result);
} catch (ExecutionException e) {
System.err.println("this attempt produce exception." + e.getCause().toString());
}
System.out.println();
}
}
接下來在Retry對象中指定監(jiān)聽:withRetryListener(new MyRetryListener<>())