重試作用:
對於重試是有場景限制的,不是什么場景都適合重試,比如參數校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試。
遠程調用超時、網絡突然中斷可以重試。在微服務治理框架中,通常都有自己的重試與超時配置,比如dubbo可以設置retries=1,timeout=500調用失敗只重試1次,超過500ms調用仍未返回則調用失敗。
比如外部 RPC 調用,或者數據入庫等操作,如果一次操作失敗,可以進行多次重試,提高調用成功的可能性。
優雅的重試機制要具備幾點:
- 無侵入:這個好理解,不改動當前的業務邏輯,對於需要重試的地方,可以很簡單的實現
- 可配置:包括重試次數,重試的間隔時間,是否使用異步方式等
- 通用性:最好是無改動(或者很小改動)的支持絕大部分的場景,拿過來直接可用
優雅重試共性和原理:
- 正常和重試優雅解耦,重試斷言條件實例或邏輯異常實例是兩者溝通的媒介。
- 約定重試間隔,差異性重試策略,設置重試超時時間,進一步保證重試有效性以及重試流程穩定性。
- 都使用了命令設計模式,通過委托重試對象完成相應的邏輯操作,同時內部封裝實現重試邏輯。
- Spring-tryer和guava-tryer工具都是線程安全的重試,能夠支持並發業務場景的重試邏輯正確性。
優雅重試適用場景:
- 功能邏輯中存在不穩定依賴場景,需要使用重試獲取預期結果或者嘗試重新執行邏輯不立即結束。比如遠程接口訪問,數據加載訪問,數據上傳校驗等等。
- 對於異常場景存在需要重試場景,同時希望把正常邏輯和重試邏輯解耦。
- 對於需要基於數據媒介交互,希望通過重試輪詢檢測執行邏輯場景也可以考慮重試方案。
優雅重試解決思路:
切面方式
這個思路比較清晰,在需要添加重試的方法上添加一個用於重試的自定義注解,然后在切面中實現重試的邏輯,主要的配置參數則根據注解中的選項來初始化
優點:
-
- 真正的無侵入
缺點:
-
- 某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
- 直接使用aspecj則有些小復雜;如果用spring-aop,則只能切被spring容器管理的bean
消息總線方式
這個也比較容易理解,在需要重試的方法中,發送一個消息,並將業務邏輯作為回調方法傳入;由一個訂閱了重試消息的consumer來執行重試的業務邏輯
優點:
-
- 重試機制不受任何限制,即在任何地方你都可以使用
- 利用
EventBus框架,可以非常容易把框架搭起來
缺點:
-
- 業務侵入,需要在重試的業務處,主動發起一條重試消息
- 調試理解復雜(消息總線方式的最大優點和缺點,就是過於靈活了,你可能都不知道什么地方處理這個消息,特別是新的童鞋來維護這段代碼時)
- 如果要獲取返回結果,不太好處理, 上下文參數不好處理
模板方式
優點:
-
- 簡單(依賴簡單:引入一個類就可以了; 使用簡單:實現抽象類,講業務邏輯填充即可;)
- 靈活(這個是真正的靈活了,你想怎么干都可以,完全由你控制)
缺點:
-
- 強侵入
- 代碼臃腫
把這個單獨撈出來,主要是某些時候我就一兩個地方要用到重試,簡單的實現下就好了,也沒有必用用到上面這么重的方式;而且我希望可以針對代碼快進行重試
這個的設計還是非常簡單的,基本上代碼都可以直接貼出來,一目了然:
public abstract class RetryTemplate {
private static final int DEFAULT_RETRY_TIME = 1;
private int retryTime = DEFAULT_RETRY_TIME;
private int sleepTime = 0;// 重試的睡眠時間
public int getSleepTime() {
return sleepTime;
}
public RetryTemplate setSleepTime(int sleepTime) {
if(sleepTime < 0) {
throw new IllegalArgumentException("sleepTime should equal or bigger than 0");
}
this.sleepTime = sleepTime;
return this;
}
public int getRetryTime() {
return retryTime;
}
public RetryTemplate setRetryTime(int retryTime) {
if (retryTime <= 0) {
throw new IllegalArgumentException("retryTime should bigger than 0");
}
this.retryTime = retryTime;
return this;
}
/**
* 重試的業務執行代碼
* 失敗時請拋出一個異常
*
* todo 確定返回的封裝類,根據返回結果的狀態來判定是否需要重試
*
* @return
*/
protected abstract Object doBiz() throws Exception; //預留一個doBiz方法由業務方來實現,在其中書寫需要重試的業務代碼,然后執行即可
public Object execute() throws InterruptedException {
for (int i = 0; i < retryTime; i++) {
try {
return doBiz();
} catch (Exception e) {
log.error("業務執行出現異常,e: {}", e);
Thread.sleep(sleepTime);
}
}
return null;
}
public Object submit(ExecutorService executorService) {
if (executorService == null) {
throw new IllegalArgumentException("please choose executorService!");
}
return executorService.submit((Callable) () -> execute());
}
}
使用示例:
public void retryDemo() throws InterruptedException {
Object ans = new RetryTemplate() {
@Override
protected Object doBiz() throws Exception {
int temp = (int) (Math.random() * 10);
System.out.println(temp);
if (temp > 3) {
throw new Exception("generate value bigger then 3! need retry");
}
return temp;
}
}.setRetryTime(10).setSleepTime(10).execute();
System.out.println(ans);
}
