背景
业务上我们常会需要一些重试的场景,比如一些对推送成功率要求较高的场景,或者调用三方接口超时的场景等。自己编写一套通用的重试代码会比较繁琐,所以就可以选用guava-retrying实现灵活的重试机制。
实践
首先我们需要引入maven依赖。
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
然后就可以愉快的使用guava-retrying进行重试了。
先定义重试器,通过构建者模式,封装各类条件和策略。
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
.withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 2, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
支持三种策略
五种重试条件
定义好重试器后,就可以执行业务代码。
try {
retryer.call(() -> {
System.out.println("开始执行");
// 判断标志物是否还存在
return false;
});
} catch (Exception e) {
System.err.println(e);
}
需要传入的是Callable的函数式接口,所以是需要返回值,retryIfResult可以通过返回值判断是否需要重试。
源码解析
了解了如何使用,还是要看一下具体是如何实现的,学习一下他们的设计思路。关于构建者模式的链式调用,我之前已经有过一篇文章,就不在多赘述了。
我们先看一下build方法,主要是将策略,条件等进行初始化处理。
public Retryer<V> build() {
AttemptTimeLimiter<V> theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.<V>noTimeLimit() : attemptTimeLimiter;
StopStrategy theStopStrategy = stopStrategy == null ? StopStrategies.neverStop() : stopStrategy;
WaitStrategy theWaitStrategy = waitStrategy == null ? WaitStrategies.noWait() : waitStrategy;
BlockStrategy theBlockStrategy = blockStrategy == null ? BlockStrategies.threadSleepStrategy() : blockStrategy;
return new Retryer<V>(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners);
}
我们的策略和条件都只能设置一个,如果相同的策略设置了两个以上,则会抛出错误。
public RetryerBuilder<V> withWaitStrategy(@Nonnull WaitStrategy waitStrategy) throws IllegalStateException {
Preconditions.checkNotNull(waitStrategy, "waitStrategy may not be null");
Preconditions.checkState(this.waitStrategy == null, "a wait strategy has already been set %s", this.waitStrategy);
this.waitStrategy = waitStrategy;
return this;
}
我们再看一下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);
}
}
}
}
整体的代码很简单,但是很清晰,基本上定时任务的每个点,都可以进行扩展,每次结果的监听,拒绝的策略,停止的策略,睡眠时间的策略,以及最后的阻塞粗略。状态的流转都是通过ResultAttempt和ExceptionAttempt进行控制。
查看ResultAttempt的构造函数
public ResultAttempt(R result, long attemptNumber, long delaySinceFirstAttempt) {
this.result = result;
this.attemptNumber = attemptNumber;
this.delaySinceFirstAttempt = delaySinceFirstAttempt;
}
@Override
public R get() throws ExecutionException {
return result;
}
查看ExceptionAttempt的构造函数
public ExceptionAttempt(Throwable cause, long attemptNumber, long delaySinceFirstAttempt) {
this.e = new ExecutionException(cause);
this.attemptNumber = attemptNumber;
this.delaySinceFirstAttempt = delaySinceFirstAttempt;
}
@Override
public R get() throws ExecutionException {
throw e;
}
最大的区别就是一个返回了执行的结果,一个返回了异常信息。
所以当判断条件不满足时,如果ExceptionAttempt则直接抛出异常。
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
总结
guava-retrying是一个简单,但是扩展性很高的重试工具类,可以很好的适配业务上的各种重试场景。