重試機制的實現(Guava Retry)
重試機制的實現
重試作用:
對于重試是有場景限制的,參數校驗不合法、寫操作等(要考慮寫是否冪等)都不適合重試。
遠程調用超時、網絡突然中斷可以重試。外部 RPC 調用,或者數據入庫等操作,如果一次操作失敗,可以進行多次重試,提高調用成功的可能性。
優雅的重試機制要具備幾點:
- 無侵入:這個好理解,不改動當前的業務邏輯,對于需要重試的地方,可以很簡單的實現
- 可配置:包括重試次數,重試的間隔時間,是否使用異步方式等
- 通用性:最好是無改動(或者很小改動)的支持絕大部分的場景,拿過來直接可用
重試實現方式
1 切面方式
在需要添加重試的方法上添加一個用于重試的自定義注解,然后在切面中實現重試的邏輯,主要的配置參數則根據注解中的選項來初始化
優點:
- 真正的無侵入
缺點:
-
某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
直接使用aspecj則有些小復雜;如果用spring-aop,則只能切被spring容器管理的bean
2 消息總線方式
這個也比較容易理解,在需要重試的方法中,發送一個消息,并將業務邏輯作為回調方法傳入;由一個訂閱了重試消息的consumer來執行重試的業務邏輯
優點:
-
重試機制不受任何限制,即在任何地方你都可以使用
利用
EventBus框架,可以非常容易把框架搭起來
缺點:
-
業務侵入,需要在重試的業務處,主動發起一條重試消息
調試理解復雜(消息總線方式的最大優點和缺點,就是過于靈活了,你可能都不知道什么地方處理這個消息,特別是新的童鞋來維護這段代碼時)
如果要獲取返回結果,不太好處理, 上下文參數不好處理
3 模板方式(定義一個抽象類,業務邏輯進行繼承) 類似與代理模式
4 spring-retry框架(注解)
Guava Retry實現以及使用
1. 使用Guava Retry
1.1 引入依賴
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
1.2 構建retryer
private static Retryer<Integer> retryer = RetryerBuilder.newBuilder()
//異常重試
.retryIfException()
//運行時異常
.retryIfRuntimeException()
//某種類型的異常
.retryIfExceptionOfType(ServiceRuntimeException.class)
//符合異常條件
.retryIfException(Predicates.equalTo(new Exception()))
//結果符合某種條件
.retryIfResult(Predicates.equalTo(false))
//重試等待時間
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
//停止條件
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//監聽器
.withRetryListener(new MyRetryListener())
//重試限制器(每一次執行的時間)
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(1, TimeUnit.SECONDS))
.build();
//監聽器
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("listener receive attempt={}",attempt);
}
})
1.3 主邏輯放在callable里,傳給retryer進行調用
public int mockQueryDB() {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return doQuery();
}
};
}
//源碼
public V call(Callable<V> callable) throws ExecutionException, RetryException{
}
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
1.4 執行
根據配置,當發生異常時,會重試,最多執行3次。每次嘗試中間會等待3秒。
如果執行3次,仍然報錯,那么retryer.call會報RetryException:
int result;
try {
result = retryer.call(callable);
//或者 返回值為泛型 最終為object 需要強制轉化
result = (int) requestRetryer.call(() -> doQuery());
} catch (Exception e) {
result = -1;
}
return result;
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2. retryIfException
guava retry支持多種條件下重試
2.1 retryIfException()
這個就是在任何異常發生時,都會進行重試。
.retryIfException()
2.2 retryIfRuntimeException()
這個是指,只有runtime exception發生時,才會進行重試。
.retryIfRuntimeException()
2.3 retryIfExceptionOfType
發生某種指定異常時,才重試。例如
.retryIfExceptionOfType(DBException.class)
2.4 retryIfException(@Nonnull Predicate exceptionPredicate)
傳入一個條件,滿足條件,就會觸發重試。
.retryIfException(e -> e.getMessage().contains("NullPointerException"))
2.5 多個retryIfException串起來時,滿足其中之一,就會觸發重試。
.retryIfExceptionOfType(DBException.class)
.retryIfException(e -> e.getMessage().contains("NullPointerException"))
3. retryIfResult
當執行沒有發生異常,但是當返回某些結果時,依然想進行重試,那么就可以使用retryIfResult。
.retryIfResult(e -> e.intValue() == 0) //當返回值為0時,會觸發重試。
4. StopStrategies
重試器的終止策略配置,默認不終止
4.1 StopStrategies.stopAfterAttempt
重試超過最大次數后終止
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
4.2 StopStrategies.stopAfterDelay
指定時間,多次嘗試直到指定時間。
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS))
4.3 StopStrategies.neverStop
一直重試,不會停止。如果不指定StopStrategies,似乎也是一樣的效果。
.withStopStrategy(StopStrategies.neverStop())
4.4 同時設置多個StopStrategies?
不能設置多個,會報錯:
java.lang.IllegalStateException: a stop strategy has already been set
com.github.rholder.retry.StopStrategies$StopAfterAttemptStrategy@21c7208d
5. WaitStrategies
重試器到的等待策略配,配置每次重試失敗后的休眠時間
5.1 WaitStrategies.fixedWait
重試前休眠固定時間
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
5.2 WaitStrategies.exponentialWait
指數增長休眠時間,2的attempTime次冪
// 第一二次之間等待 2 ms,接下來等 2*2 ms, 2*2*2 ms, ...
.withWaitStrategy(WaitStrategies.exponentialWait())
//指數等待時間,最多10s。超過10s,也只等待10s。
.withWaitStrategy(WaitStrategies.exponentialWait(10, TimeUnit.SECONDS))
// 等待時間乘以 5的系數。指數級增長,最多不超過10s.
.withWaitStrategy(WaitStrategies.exponentialWait(5, 10, TimeUnit.SECONDS))
5.3 WaitStrategies.fibonacciWait
以斐波那契數列的方式增長。參數含義與exponentialWait類似。
.withWaitStrategy(WaitStrategies.fibonacciWait())
.withWaitStrategy(WaitStrategies.fibonacciWait(10, TimeUnit.SECONDS))
.withWaitStrategy(WaitStrategies.fibonacciWait(5, 10, TimeUnit.SECONDS))
5.4 WaitStrategies.exceptionWait
對于不同的異常類型,定義不同的等待時間策略。
.withWaitStrategy(WaitStrategies.exceptionWait(DBException.class, x -> 50l))
5.5 WaitStrategies.randomWait
重試前休眠minimumTime~maximumTime之間隨機時間
//等待時間為 0 到3秒 之間的隨機時間
.withWaitStrategy(WaitStrategies.randomWait(3, TimeUnit.SECONDS))
//等待時間為 1秒到3秒之間的隨機時間
.withWaitStrategy(WaitStrategies.randomWait(1, TimeUnit.SECONDS, 3, TimeUnit.SECONDS))
5.6 WaitStrategies.incrementingWait
第一次重試休眠initialSleepTime,后續每次重試前休眠時間線性遞增increment。例如,第一二次之間等待1秒,接下來每次增加3秒。
.withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 3, TimeUnit.SECONDS))
5.7 WaitStrategies.noWait
不等待,直接重試。
.withWaitStrategy(WaitStrategies.noWait())
5.8 WaitStrategies.join (CompositeWaitStrategy)
WaitStrategies.join可以將多種等待策略組合起來,等待時間為多個策略的時間和。
例如,join了exponentialWait和fixedWait:
.withWaitStrategy(WaitStrategies.join(WaitStrategies.exponentialWait(100, 5, TimeUnit.SECONDS),
WaitStrategies.fixedWait(1, TimeUnit.SECONDS)))
6 監聽器
public class MyRetryListener<String> implements RetryListener {
@Override
public <String> void onRetry(Attempt<String> attempt) {
// 第幾次重試,(注意:第一次重試其實是第一次調用)
System.out.print("[retry]time=" + attempt.getAttemptNumber());
// 距離第一次重試的延遲
System.out.print("[retry]delay=" + attempt.getDelaySinceFirstAttempt());
// 重試結果: 是異常終止, 還是正常返回
System.out.print("[retry]hasException=" + attempt.hasException());
System.out.print("[retry]hasResult=" + attempt.hasResult());
// 是什么原因導致異常
if (attempt.hasException()) {
System.out.print("[retry]causeBy=" + attempt.getExceptionCause().toString());
} else {
// 正常返回時的結果
System.out.print("[retry]result=" + attempt.getResult());
}
// bad practice: 增加了額外的異常處理代碼
try {
String result = attempt.get();
System.out.print("rude get=" + result);
} catch (ExecutionException e) {
System.err.println("this attempt produce exception." + e.getCause().toString());
}
}
}
posted on 2022-12-02 10:38 Chase_Hanky 閱讀(771) 評論(0) 收藏 舉報
浙公網安備 33010602011771號