重试在项目中还是比较常见的一个场景,比如调用外部服务因为网络等原因的异常,重试一次可能就成功了,而不需要立即给用户反馈错误,提高体验
假设我们自己来写一个最简单异常重试的话,可能代码是这样子的
1 2 3 4 5 6 7 8 9
| int retryTime = 3; for (int i = 0; i < retryTime; i++) { try { return service.query(); } catch (Throwable e) { log.info("调用异常,进行重试"); } }
|
这里可能有几个问题需要考虑
- 不仅仅是异常需要重试,有时接口返回的特定错误码也是需要重试的
- 不是所有的异常都可以重试,需要根据情况判断异常原因才能重试
- 有时失败不能立即重试,需要等待一小段时间,比如短时网络波动
- 重试停止不一定只需要次数,有时也需要判断整体用的时间等因素
这么一看,需要考虑的地方还挺多,而重试功能又是一个非常通用的功能,所以完全可以包装一下做成通用的能力
而这个目前已经有一些现成的工具供我们使用,这次我们就先看下 guava-retrying
快速使用
首先需要引入依赖包
1 2 3 4 5
| <dependency> <groupId>com.github.rholder</groupId> <artifactId>guava-retrying</artifactId> <version>2.0.0</version> </dependency>
|
然后就可以使用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| Callable<Boolean> callable = new Callable<Boolean>() { public Boolean call() throws Exception { return true; } };
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(Predicates.<Boolean>isNull()) .retryIfExceptionOfType(IOException.class) .retryIfRuntimeException() .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(200, TimeUnit.MILLISECONDS)) .withWaitStrategy(WaitStrategies.fixedWait(100, TimeUnit.MILLISECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build(); try { retryer.call(callable); } catch (RetryException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }
|
实现分析
可以看到整体源码比较简单清晰
我们直接通过调用方法的代码来看一下各个类的用途
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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); } } } }
|
这样我们就比较好理解下面的类的作用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| . ├── Attempt // 执行的结果包装(可能成功或失败) 【接口】 ├── AttemptTimeLimiter // 执行时间限制接口(实际方法由它执行)【接口】 ├── AttemptTimeLimiters // 执行时间限制接口实现工厂类【类】 ├───── FixedAttemptTimeLimit // 固定限制执行时间【类】 ├───── NoAttemptTimeLimit // 不限制执行时间【类】 ├── BlockStrategy // 执行阻塞策略接口【接口】 ├── BlockStrategies // 执行阻塞策略工厂类【类】 ├───── ThreadSleepStrategy // 通过Thread.sleep进行阻塞的实现【类】 ├── RetryException // 重试异常类【类】 ├── RetryListener // 重试监听器接口【接口】 ├── Retryer // 实际执行重试器【类】 ├───── ExceptionAttempt // 执行异常的结果包装【类】 ├───── ResultAttempt // 执行成功的结果包装【类】 ├───── RetryerCallable // 没有使用..【类】 ├── RetryerBuilder // 重试构造器【类】 ├───── ExceptionClassPredicate // 用于异常类型判断是否需要重试断言【类】 ├───── ExceptionPredicate // 根据异常进行判断是否需要重试断言【类】 ├───── ResultPredicate // 根据执行结果判断是否需要重试断言【类】 ├── StopStrategy // 终止策略【接口】 ├── StopStrategies // 终止策略实现工厂类【类】 ├───── NeverStopStrategy // 永不终止【类】 ├───── StopAfterAttemptStrategy // 限制最多重试次数进行终止【类】 ├───── StopAfterDelayStrategy // 限制总执行时间进行终止【类】 ├── WaitStrategy // 等待时间计算策略【接口】 ├── WaitStrategies // 等待时间计算策略实现工厂【类】 ├───── CompositeWaitStrategy // 复合等待时间计算类(聚合其他实现类)【类】 ├───── ExceptionWaitStrategy // 根据特定类型异常计算等待时间【类】 ├───── ExponentialWaitStrategy // 每次等待时间进行指数增长【类】 ├───── FibonacciWaitStrategy // 每次等待时间进行fibonacci方式增长【类】 ├───── FixedWaitStrategy // 每次都是固定的等待时间【类】 ├───── IncrementingWaitStrategy // 根据初始值和指定值,每次重试增加指定值时间【类】 └───── RandomWaitStrategy // 指定一个时间范围,每次在其中进行随机取值【类】
|
其中提供了很多可以直接使用的策略类,当不满足我们需求的时候,也可以自己实现对应的接口来扩展