前言
小黑在開發中遇到個問題,我負責的模組需要呼叫某個三方服務介面查詢資訊,查詢結果直接影響後續業務邏輯的處理;
這個介面偶爾會因網路問題出現超時,導致我的業務邏輯無法繼續處理;
這個問題該如何解決呢?,小黑首先想到的就是重試嘛,如果失敗了就再呼叫一次。
問題來了,如果又失敗了呢?接著重試嘛。我們迴圈處理,比如迴圈5次,全失敗則任務服務不可用,結束呼叫。
如果我又想著5次呼叫間隔一段時間呢?第一次先隔1秒,然後3秒,然後5秒呢?
小黑發現事情沒那麼簡單,如果自己搞容易出BUG呀。
轉念一想,這個常見挺常見,網上應該有輪子呀,找找看。一不小心就讓我給找著啦,哈哈。
Guava Retryer
This is a small extension to Google’s Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.
使用Guava Retryer你可以自定義來執行重試,同時也可以監控每次重試的結果和行為,最重要的基於 Guava 風格的重試方式真的很方便。
引入依賴
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
快速開始
Callable<Boolean> callable = new Callable<Boolean>() {
public Boolean call() throws Exception {
return true; // do something useful here
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.<Boolean>isNull()) // callable返回null時重試
.retryIfExceptionOfType(IOException.class) // callable丟擲IOException重試
.retryIfRuntimeException() // callable丟擲RuntimeException重試
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 重試3次後停止
.build();
try {
retryer.call(callable);
} catch (RetryException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
在Callable
的call()
方法返回null,丟擲IOException
或者RuntimeException
時會重試;
將在嘗試重試3次後停止,並丟擲包含上次失敗嘗試資訊的RetryException
;
如果call()方法中彈出任何其他異常,它將被包裝並在ExecutionException
中重新呼叫。
指數退避(Exponential Backoff)
根據wiki上對Exponential backoff
的說明,指數補償是一種通過反饋,成倍地降低某個過程的速率,以逐漸找到合適速率的演算法。
在乙太網中,該演算法通常用於衝突後的排程重傳。根據時隙和重傳嘗試次數來決定延遲重傳。
在c
次碰撞後(比如請求失敗),會選擇0和2^c - 1
之間的隨機值作為時隙的數量。
對於第1次碰撞來說,每個傳送者將會等待0或1個時隙進行傳送。
而在第2次碰撞後,傳送者將會等待0到3( 由2^2 -1
計算得到)個時隙進行傳送。
而在第3次碰撞後,傳送者將會等待0到7( 由2^3 - 1
計算得到)個時隙進行傳送。
以此類推……
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES)) // 指數退避
.withStopStrategy(StopStrategies.neverStop()) // 永遠不停止重試
.build();
建立一個永遠重試的重試器,在每次重試失敗後以指數級退避間隔遞增,直到最多5分鐘。5分鐘後,從那時起每隔5分鐘重試一次。
斐波那契退避(Fibonacci Backoff)
斐波那契數列指的是這樣一個數列:
0,1,1,2,3,5,8,13,21,34,55,89...
這個數列從第3項開始,每一項都等於前兩項之和。
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.fibonacciWait(100, 2, TimeUnit.MINUTES)) // 斐波那契退避
.withStopStrategy(StopStrategies.neverStop())
.build();
建立一個永遠重試的重試器,在每次重試失敗後以增加斐波那契退避間隔的方式等待,直到最多2分鐘。2分鐘後,從那時起每隔2分鐘重試一次。
與指數退避策略類似,斐波那契退避策略遵循一種模式,即在每次嘗試失敗後等待的時間越來越長。
對於這兩種策略的效能英國利茲大學專門做過效能測試,相比指數退避策略,斐波那契退避策略可能效能更好,吞吐量可能也更好。
重試監聽器
當重試發生時,如果需要額外做一些動作,比如傳送郵件通知之類的,可以通過RetryListener
,Guava Retryer在每次重試之後會自動回撥監聽器,並且支援註冊多個監聽。
@Slf4j
class DiyRetryListener<Boolean> implements RetryListener {
@Override
public <Boolean> void onRetry(Attempt<Boolean> attempt) {
log.info("重試次數:{}",attempt.getAttemptNumber());
log.info("距離第一次重試的延遲:{}",attempt.getDelaySinceFirstAttempt());
if(attempt.hasException()){
log.error("異常原因:",attempt.getExceptionCause());
}else {
System.out.println("正常處理結果:{}" + attempt.getResult());
}
}
}
定義監聽器之後,需要在Retryer中進行註冊。
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.<Boolean>isNull()) // callable返回null時重試
.retryIfExceptionOfType(IOException.class) // callable丟擲IOException重試
.retryIfRuntimeException() // callable丟擲RuntimeException重試
.withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 重試3次後停止
.withRetryListener(new DiyRetryListener<Boolean>()) // 註冊監聽器
.build();
小結
Guava Retryer不光在重試策略上支援多種選擇,並且將業務邏輯的處理放在Callable
中,和重試處理邏輯分開,實現瞭解耦,這比小黑自己去寫迴圈處理要優秀太多啦,Guava確實強大。