guava-retrying是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类,官网:GitHub - rholder/guava-retrying: 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.https://github.com/rholder/guava-retrying
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
看一个使用例子:
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfException()
.retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()))
.withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.print("retry time=" + attempt.getAttemptNumber());
}
}).build();
try {
retryer.call(() -> {
// 逻辑处理
return null;
});
} catch (Exception e) {
System.out.println("exception:" + e);
}
1、常用类介绍
1)Attemp
Attemp既是一次任务重试(call),也是一次请求的结果,记录了当前请求的重试次数,是否包含异常和请求的返回值。我们可以配合监听器使用,用于记录重试过程的细节,常用的方法有如下几个:
- getAttemptNumber(),表示准备开始第几次重试;
- getDelaySinceFirstAttempt(),表示距离第一次重试的延迟,也就是与第一次重试的时间差,单位毫秒;
- hasException(),表示是异常导致的重试还是正常返回;
- hasResult(),表示是否返回了结果;因为有时候是因为返回了特定结果才进行重试;
- getExceptionCause(),如果是异常导致的重试,那么获取具体具体的异常类型;
- getResult(),返回重试的结果;
- get(),如果有的话,返回重试的结果;和getResult不同的在于对异常的处理;
2)Retryer:
Retryer是最核心的类,是用于执行重试策略的类,通过RetryerBuilder类进行构造,并且RetryerBuilder负责将设置好的重试策咯添加到Retryer中,最终通过执行Retryer的核心方法call来执行重试策略(一次任务的执行是如何进行的?),call方法源码如下:
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
for (int attemptNumber = 1; ; attemptNumber++) {
Attempt<V> attempt;
try {
V result = attemptTimeLimiter.call(callable);//任务执行的时间限制
attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) {
attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
for (RetryListener listener : listeners) {
listener.onRetry(attempt);
}
// 判断是否满足重试条件,来决定是否继续等待并进行重试
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);
}
}
}
}
大致流程如下:
- 判断是否超过任务时长限制;
- 执行重试listener
- 判断是否满足重试条件
- 重试停止策略;
- 重试等待策略;
- 组册策略;
3)RetryListener:
当执行call方法时,会调用监听器中的onRetry方法。重写该类的onRetry方法可以实现自定义逻辑,例如:
@Slf4j
public class RetryLogListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
// 第几次重试,(注意:第一次重试其实是第一次调用)
log.info("retry time : [{}]", attempt.getAttemptNumber());
// 距离第一次重试的延迟
log.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());
// 重试结果: 是异常终止, 还是正常返回
log.info("hasException={}", attempt.hasException());
log.info("hasResult={}", attempt.hasResult());
// 是什么原因导致异常
if (attempt.hasException()) {
log.info("causeBy={}" , attempt.getExceptionCause().toString());
} else {
// 正常返回时的结果
log.info("result={}" , attempt.getResult());
}
log.info("log listen over.");
}
}
2、WaitStrategies 重试等待策略
当执行失败后,用来指定不同的等待策略来进行第二、三...次的重试。
通过withWaitStrategy方法可以设置不同的重试等待策略,WaitStrategies.java中定义了很多重试等待方法,常见的有:
1)ExponentialWaitStrategy 指数等待策略
实现了指数补偿算法(Exponential Backoff),其源码根据重试次数计算等待时长,如下:
@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;
}
例如:
WaitStrategies.exponentialWait() . #默认乘数multiplier是1,最大值是Long.MAX_VALUE
也可以指定乘数和最大值:
.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))
第一次失败后,依次等待时长:2^1 * 100;2^2 * 100;2^3 * 100;...直到最多5分钟。 5分钟后,每隔5分钟重试一次。
2)FibonacciWaitStrategy 斐波那契等待策略
失败后,按照斐波那契数列进行等待,例如:
WaitStrategies.fibonacciWait() #默认乘数multiplier是1,最大值是Long.MAX_VALUE
也可以指定乘数和最大值
.withWaitStrategy(WaitStrategies.fibonacciWait(100, 2, TimeUnit.MINUTES))
第一次失败后,依次等待时长:1*100;1*100;2*100;3*100;5*100;...直到最多2分钟,2分钟后每隔2分钟重试一次;
3)FixedWaitStrategy 固定时长等待策略
失败后,将等待固定的时长进行重试,例如:
withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
4)RandomWaitStrategy 随机时长等待策略
可以设置一个随机等待的最大时长,也可以设置一个随机等待的时长区间,例如:
withWaitStrategy(WaitStrategies.randomWait(10, TimeUnit.SECONDS));
withWaitStrategy(WaitStrategies.randomWait(1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS));
5)IncrementingWaitStrategy 递增等待策略
根据初始值和递增值,等待时长依次递增。例如:
withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 5, TimeUnit.SECONDS))
第一次失败后,将依次等待1s;6s(1+5);11(1+5+5)s;16(1+5+5+5)s;...
6)ExceptionWaitStrategy 异常等待策略
根据所发生的异常指定重试的等待时长;如果异常不匹配,则等待时长为0;
withWaitStrategy(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L))
7)CompositeWaitStrategy 复合等待策略
如果所执行的程序满足一个或多个等待策略,那么等待时间为所有等待策略时间的总和。例如:
.withWaitStrategy(WaitStrategies.join(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L),WaitStrategies.fixedWait(5, TimeUnit.SECONDS)))
3、BlockStrategies 阻塞策略
在重试等待过程中,根据等待策略计算的时间,来阻塞。默认只提供一种阻塞策略:ThreadSleepStrategy,实现方式是通过Thread.sleep(sleepTime)来实现
默认的阻塞策略是线程休眠,可以自定义阻塞策略,这里使用自旋锁实现,不阻塞线程。
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy;
import com.github.rholder.retry.BlockStrategy;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 自旋锁的实现, 不响应线程中断
*/
@Slf4j
@NoArgsConstructor
public class SpinBlockStrategy implements BlockStrategy {
@Override
public void block(long sleepTime) throws InterruptedException {
LocalDateTime startTime = LocalDateTime.now();
long start = System.currentTimeMillis();
long end = start;
log.info("[SpinBlockStrategy]...begin wait.");
while (end - start <= sleepTime) {
end = System.currentTimeMillis();
}
//使用Java8新增的Duration计算时间间隔
Duration duration = Duration.between(startTime, LocalDateTime.now());
log.info("[SpinBlockStrategy]...end wait.duration={}", duration.toMillis());
}
}
使用时:
//自定义阻塞策略:自旋锁
.withBlockStrategy(new SpinBlockStrategy())
4、StopStrategies 重试停止策略
用来指定重试多少次后停止重试。因为无限制的重试不一定是一个好的方式,可能会给下游系统带来灾难性。
通过withStopStrategy方法可以设置重试停止策略,StopStrategies.java中定义了很多重试停止策略,常见的有:
1)NeverStopStrategy
无限重试,例如:
withStopStrategy(StopStrategies.neverStop())
2)StopAfterAttemptStrategy
重试n次后,停止;例如:
withStopStrategy(StopStrategies.stopAfterAttempt(3))
3)StopAfterDelayStrategy
重试多长时间后,停止;例如:
withStopStrategy(StopStrategies.stopAfterDelay(3, TimeUnit.MINUTES))
5、AttemptTimeLimiters 任务执行时长限制
这个表示单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
通过withAttemptTimeLimiter方法可以设置任务的执行时间限制,常见的有:
1)NoAttemptTimeLimit 无时长限制
顾名思义,不限制执行时长;每次都是等执行任务执行完成之后,才进行后续的重试策咯。
.withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit())
2) FixedAttemptTimeLimit
可以指定任务的执行时长限制,并且为了控制线程管理,最好指定相应的线程池。
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS));
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()));
参考: