前言
guava-retrying github地址:https://github.com/rholder/guava-retrying
guava-retrying是谷歌的Guava庫的一個小擴展衙解,允許為任意函數(shù)調(diào)用創(chuàng)建可配置的重試策略,比如與正常運行時間不穩(wěn)定的遠程服務對話的函數(shù)調(diào)用焰枢。
在日常開發(fā)中蚓峦,尤其是在微服務盛行的時代下,我們在調(diào)用外部接口時济锄,經(jīng)常會因為第三方接口超時暑椰、限流等問題從而造成接口調(diào)用失敗,那么此時我們通常會對接口進行重試荐绝,那么問題來了一汽,如何重試呢?該重試幾次呢?如果要設置重試時間超過多長時間后還不成功就不重試了該怎么做呢召夹?所幸guava-retrying為我們提供了強大而簡單易用的重試框架guava-retrying岩喷。
一、pom依賴
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
二监憎、使用示例
我們可以通過RetryerBuilder來構(gòu)造一個重試器纱意,通過RetryerBuilder可以設置什么時候需要重試(即重試時機)、停止重試策略鲸阔、失敗等待時間間隔策略偷霉、任務執(zhí)行時長限制策略
先看一個簡單的例子:
private int invokeCount = 0;
public int realAction(int num) {
invokeCount++;
System.out.println(String.format("當前執(zhí)行第 %d 次,num:%d", invokeCount, num));
if (num <= 0) {
throw new IllegalArgumentException();
}
return num;
}
@Test
public void guavaRetryTest001() {
Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
// 非正數(shù)進行重試
.retryIfRuntimeException()
// 偶數(shù)則進行重試
.retryIfResult(result -> result % 2 == 0)
// 設置最大執(zhí)行次數(shù)3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();
try {
invokeCount=0;
retryer.call(() -> realAction(0));
} catch (Exception e) {
System.out.println("執(zhí)行0,異常:" + e.getMessage());
}
try {
invokeCount=0;
retryer.call(() -> realAction(1));
} catch (Exception e) {
System.out.println("執(zhí)行1褐筛,異常:" + e.getMessage());
}
try {
invokeCount=0;
retryer.call(() -> realAction(2));
} catch (Exception e) {
System.out.println("執(zhí)行2类少,異常:" + e.getMessage());
}
}
輸出:
當前執(zhí)行第 1 次,num:0
當前執(zhí)行第 2 次,num:0
當前執(zhí)行第 3 次,num:0
執(zhí)行0,異常:Retrying failed to complete successfully after 3 attempts.
當前執(zhí)行第 1 次,num:1
當前執(zhí)行第 1 次,num:2
當前執(zhí)行第 2 次,num:2
當前執(zhí)行第 3 次,num:2
執(zhí)行2渔扎,異常:Retrying failed to complete successfully after 3 attempts.
三硫狞、重試時機
RetryerBuilder的retryIfXXX()方法用來設置在什么情況下進行重試,總體上可以分為根據(jù)執(zhí)行異常進行重試和根據(jù)方法執(zhí)行結(jié)果進行重試兩類赞警。
3.1 根據(jù)異常進行重試
方法 | 描述 |
---|---|
retryIfException() | 當方法執(zhí)行拋出異常 isAssignableFrom Exception.class 時重試 |
retryIfRuntimeException() | 當方法執(zhí)行拋出異常 isAssignableFrom RuntimeException.class 時重試 |
retryIfException(Predicate<Throwable> exceptionPredicate) | 這里當發(fā)生異常時妓忍,會將異常傳遞給exceptionPredicate,那我們就可以通過傳入的異常進行更加自定義的方式來決定什么時候進行重試 |
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass) | 當方法執(zhí)行拋出異常 isAssignableFrom 傳入的exceptionClass 時重試 |
3.2 根據(jù)返回結(jié)果進行重試
retryIfResult(@Nonnull Predicate<V> resultPredicate)
這個比較簡單愧旦,當我們傳入的resultPredicate返回true時則進行重試
四世剖、停止重試策略StopStrategy
停止重試策略用來決定什么時候不進行重試,其接口com.github.rholder.retry.StopStrategy笤虫,停止重試策略的實現(xiàn)類均在com.github.rholder.retry.StopStrategies中旁瘫,它是一個策略工廠類。
public interface StopStrategy {
/**
* Returns <code>true</code> if the retryer should stop retrying.
*
* @param failedAttempt the previous failed {@code Attempt}
* @return <code>true</code> if the retryer must stop, <code>false</code> otherwise
*/
boolean shouldStop(Attempt failedAttempt);
}
4.1 NeverStopStrategy
此策略將永遠重試琼蚯,永不停止酬凳,查看其實現(xiàn)類,直接返回了false
@Override
public boolean shouldStop(Attempt failedAttempt) {
return false;
}
4.2 StopAfterAttemptStrategy
當執(zhí)行次數(shù)到達指定次數(shù)之后停止重試遭庶,查看其實現(xiàn)類:
private static final class StopAfterAttemptStrategy implements StopStrategy {
private final int maxAttemptNumber;
public StopAfterAttemptStrategy(int maxAttemptNumber) {
Preconditions.checkArgument(maxAttemptNumber >= 1, "maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
this.maxAttemptNumber = maxAttemptNumber;
}
@Override
public boolean shouldStop(Attempt failedAttempt) {
return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
}
}
4.3 StopAfterDelayStrategy
當距離方法的第一次執(zhí)行超出了指定的delay時間時停止宁仔,也就是說一直進行重試,當進行下一次重試的時候會判斷從第一次執(zhí)行到現(xiàn)在的所消耗的時間是否超過了這里指定的delay時間峦睡,查看其實現(xiàn):
private static final class StopAfterAttemptStrategy implements StopStrategy {
private final int maxAttemptNumber;
public StopAfterAttemptStrategy(int maxAttemptNumber) {
Preconditions.checkArgument(maxAttemptNumber >= 1, "maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
this.maxAttemptNumber = maxAttemptNumber;
}
@Override
public boolean shouldStop(Attempt failedAttempt) {
return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
}
}
五翎苫、重試間隔策略WaitStrategy以及重試阻塞策略BlockStrategy
這兩個策略放在一起說,它們合起來的作用就是用來控制重試任務之間的間隔時間榨了,以及如何任務在等待時間間隔時如何阻塞煎谍。也就是說WaitStrategy決定了重試任務等待多久后進行下一次任務的執(zhí)行,BlockStrategy用來決定任務如何等待龙屉。它們兩的策略工廠分別為com.github.rholder.retry.WaitStrategies和BlockStrategies呐粘。
5.1 BlockStrategy
5.1.1 ThreadSleepStrategy
這個是BlockStrategies,決定如何阻塞任務,其主要就是通過Thread.sleep()來進行阻塞的作岖,查看其實現(xiàn):
@Immutable
private static class ThreadSleepStrategy implements BlockStrategy {
@Override
public void block(long sleepTime) throws InterruptedException {
Thread.sleep(sleepTime);
}
}
5.2 WaitStrategy
5.2.1 IncrementingWaitStrategy
該策略在決定任務間隔時間時唆垃,返回的是一個遞增的間隔時間,即每次任務重試間隔時間逐步遞增鳍咱,越來越長降盹,查看其實現(xiàn):
private static final class IncrementingWaitStrategy implements WaitStrategy {
private final long initialSleepTime;
private final long increment;
public IncrementingWaitStrategy(long initialSleepTime,
long increment) {
Preconditions.checkArgument(initialSleepTime >= 0L, "initialSleepTime must be >= 0 but is %d", initialSleepTime);
this.initialSleepTime = initialSleepTime;
this.increment = increment;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1));
return result >= 0L ? result : 0L;
}
}
該策略輸入一個起始間隔時間值和一個遞增步長,然后每次等待的時長都遞增increment時長谤辜。
5.2.2 RandomWaitStrategy
顧名思義蓄坏,返回一個隨機的間隔時長,我們需要傳入的就是一個最小間隔和最大間隔丑念,然后隨機返回介于兩者之間的一個間隔時長涡戳,其實現(xiàn)為:
private static final class RandomWaitStrategy implements WaitStrategy {
private static final Random RANDOM = new Random();
private final long minimum;
private final long maximum;
public RandomWaitStrategy(long minimum, long maximum) {
Preconditions.checkArgument(minimum >= 0, "minimum must be >= 0 but is %d", minimum);
Preconditions.checkArgument(maximum > minimum, "maximum must be > minimum but maximum is %d and minimum is", maximum, minimum);
this.minimum = minimum;
this.maximum = maximum;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum);
return t + minimum;
}
}
5.2.3 FixedWaitStrategy
該策略是返回一個固定時長的重試間隔。查看其實現(xiàn):
private static final class FixedWaitStrategy implements WaitStrategy {
private final long sleepTime;
public FixedWaitStrategy(long sleepTime) {
Preconditions.checkArgument(sleepTime >= 0L, "sleepTime must be >= 0 but is %d", sleepTime);
this.sleepTime = sleepTime;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
return sleepTime;
}
}
5.2.4 ExceptionWaitStrategy
該策略是由方法執(zhí)行異常來決定是否重試任務之間進行間隔等待脯倚,以及間隔多久渔彰。
private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
private final Class<T> exceptionClass;
private final Function<T, Long> function;
public ExceptionWaitStrategy(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
this.exceptionClass = exceptionClass;
this.function = function;
}
@SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ConstantConditions", "unchecked"})
@Override
public long computeSleepTime(Attempt lastAttempt) {
if (lastAttempt.hasException()) {
Throwable cause = lastAttempt.getExceptionCause();
if (exceptionClass.isAssignableFrom(cause.getClass())) {
return function.apply((T) cause);
}
}
return 0L;
}
}
5.2.5 CompositeWaitStrategy
這個沒啥好說的,顧名思義推正,就是一個策略的組合恍涂,你可以傳入多個WaitStrategy,然后所有WaitStrategy返回的間隔時長相加就是最終的間隔時間植榕。查看其實現(xiàn):
private static final class CompositeWaitStrategy implements WaitStrategy {
private final List<WaitStrategy> waitStrategies;
public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) {
Preconditions.checkState(!waitStrategies.isEmpty(), "Need at least one wait strategy");
this.waitStrategies = waitStrategies;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
long waitTime = 0L;
for (WaitStrategy waitStrategy : waitStrategies) {
waitTime += waitStrategy.computeSleepTime(failedAttempt);
}
return waitTime;
}
}
5.2.6 FibonacciWaitStrategy
這個策略與IncrementingWaitStrategy有點相似再沧,間隔時間都是隨著重試次數(shù)的增加而遞增的,不同的是尊残,F(xiàn)ibonacciWaitStrategy是按照斐波那契數(shù)列來進行計算的炒瘸,使用這個策略時,我們需要傳入一個乘數(shù)因子和最大間隔時長寝衫,其實現(xiàn)就不貼了
5.2.7 ExponentialWaitStrategy
這個與IncrementingWaitStrategy顷扩、FibonacciWaitStrategy也類似,間隔時間都是隨著重試次數(shù)的增加而遞增的慰毅,但是該策略的遞增是呈指數(shù)級遞增隘截。查看其實現(xiàn):
private static final class ExponentialWaitStrategy implements WaitStrategy {
private final long multiplier;
private final long maximumWait;
public ExponentialWaitStrategy(long multiplier,
long maximumWait) {
Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", multiplier);
Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", maximumWait);
Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier);
this.multiplier = multiplier;
this.maximumWait = maximumWait;
}
@Override
public long computeSleepTime(Attempt failedAttempt) {
double exp = Math.pow(2, failedAttempt.getAttemptNumber());
long result = Math.round(multiplier * exp);
if (result > maximumWait) {
result = maximumWait;
}
return result >= 0L ? result : 0L;
}
}
六、重試監(jiān)聽器RetryListener
當發(fā)生重試時汹胃,將會調(diào)用RetryListener的onRetry方法婶芭,此時我們可以進行比如記錄日志等額外操作。
public int realAction(int num) {
if (num <= 0) {
throw new IllegalArgumentException();
}
return num;
}
@Test
public void guavaRetryTest001() throws ExecutionException, RetryException {
Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder().retryIfException()
.withRetryListener(new MyRetryListener())
// 設置最大執(zhí)行次數(shù)3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();
retryer.call(() -> realAction(0));
}
private static class MyRetryListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.println("第" + attempt.getAttemptNumber() + "次執(zhí)行");
}
}
輸出:
第1次執(zhí)行
第2次執(zhí)行
第3次執(zhí)行
七统台、重試原理
其實到這一步之后雕擂,實現(xiàn)原理大概就很清楚了啡邑,就是由上述各種策略配合從而達到了非常靈活的重試機制贱勃。在這之前我們看一個上面沒說的東東-Attempt
public interface Attempt<V> {
public V get() throws ExecutionException;
public boolean hasResult();
public boolean hasException();
public V getResult() throws IllegalStateException;
public Throwable getExceptionCause() throws IllegalStateException;
public long getAttemptNumber();
public long getDelaySinceFirstAttempt();
}
通過接口方法可以知道Attempt這個類包含了任務執(zhí)行次數(shù)、任務執(zhí)行異常、任務執(zhí)行結(jié)果贵扰、以及首次執(zhí)行任務至今的時間間隔仇穗,那么我們后續(xù)的不管重試時機、還是其他策略都是根據(jù)此值來決定戚绕。
接下來看關鍵執(zhí)行入口Retryer#call:
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
// 執(zhí)行次數(shù)從1開始
for (int attemptNumber = 1; ; attemptNumber++) {
Attempt<V> attempt;
try {
// 嘗試執(zhí)行
V result = attemptTimeLimiter.call(callable);
// 執(zhí)行成功則將結(jié)果封裝為ResultAttempt
attempt = new Retryer.ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) {
// 執(zhí)行異常則將結(jié)果封裝為ExceptionAttempt
attempt = new Retryer.ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
// 這里將執(zhí)行結(jié)果傳給RetryListener做一些額外事情
for (RetryListener listener : listeners) {
listener.onRetry(attempt);
}
// 這個就是決定是否要進行重試的地方纹坐,如果不進行重試直接返回結(jié)果,執(zhí)行成功就返回結(jié)果舞丛,執(zhí)行失敗就返回異常
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
// 到這里耘子,說明需要進行重試,則此時先決定是否到達了停止重試的時機球切,如果到達了則直接返回異常
if (stopStrategy.shouldStop(attempt)) {
throw new RetryException(attemptNumber, attempt);
} else {
// 決定重試時間間隔
long sleepTime = waitStrategy.computeSleepTime(attempt);
try {
// 進行阻塞
blockStrategy.block(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
八谷誓、總結(jié)
通篇下來可以看到其實核心實現(xiàn)并不難,但是此框架通過建造者模式和策略模式組合運用吨凑,提供了十分清晰明了且靈活的重試機制捍歪,其設計思路還是值得借鑒學習!