重试的使用场景
在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。
尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。
通常情况下,我们会通过定时任务进行重试。例如某次操作失败,则记录下来,当定时任务再次启动,则将数据放到定时任务的方法中,重新跑一遍。最终直至得到想要的结果为止。
无论是基于定时任务的重试机制,还是我们自己写的简单的重试器,缺点都是重试的机制太单一,而且实现起来不优雅。
如何优雅地设计重试实现
一个完备的重试实现,要很好地解决如下问题:
- 什么条件下重试
- 什么条件下停止
- 如何停止重试
- 停止重试等待多久
- 如何等待
- 请求时间限制
- 如何结束
- 如何监听整个重试过程
并且,为了更好地封装性,重试的实现一般分为两步:
- 使用工厂模式构造重试器
- 执行重试方法并得到结果
一个完整的重试流程可以简单示意为:
guava-retrying基础用法
guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器。
下面就快速看一下它的用法。
1.Maven配置
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
需要注意的是,此版本依赖的是27.0.1版本的guava。如果你项目中的guava低几个版本没问题,但是低太多就不兼容了。这个时候你需要升级你项目的guava版本,或者直接去掉你自己的guava依赖,使用guava-retrying传递过来的guava依赖。
2.实现Callable
Callable<Boolean> callable = new Callable<Boolean>() {
public Boolean call() throws Exception {
return true; // do something useful here
}
};
Callable的call方法中是你自己实际的业务调用。
3.通过RetryerBuilder构造Retryer
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.<Boolean>isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
4.使用重试器执行你的业务
retryer.call(callable);
下面是完整的参考实现。
public Boolean test() throws Exception {
//定义重试机制
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
//retryIf 重试条件
.retryIfException()
.retryIfRuntimeException()
.retryIfExceptionOfType(Exception.class)
.retryIfException(Predicates.equalTo(new Exception()))
.retryIfResult(Predicates.equalTo(false))
//等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
//停止策略 : 尝试请求6次
.withStopStrategy(StopStrategies.stopAfterAttempt(6))
//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
.build();
//定义请求实现
Callable<Boolean> callable = new Callable<Boolean>() {
int times = 1;
@Override
public Boolean call() throws Exception {
log.info("call times={}", times);
times++;
if (times == 2) {
throw new NullPointerException();
} else if (times == 3) {
throw new Exception();
} else if (times == 4) {
throw new RuntimeException();
} else if (times == 5) {
return false;
} else {
return true;
}
}
};
//利用重试器调用请求
return retryer.call(callable);
}
guava-retrying实现原理
guava-retrying的核心是Attempt类、Retryer类以及一些Strategy(策略)相关的类。
Attempt
Attempt既是一次重试请求(call),也是请求的结果,并记录了当前请求的次数、是否包含异常和请求的返回值。
/**
* An attempt of a call, which resulted either in a result returned by the call,
* or in a Throwable thrown by the call.
*
* @param <V> The type returned by the wrapped callable.
* @author JB
*/
public interface Attempt<V>
Retryer
Retryer通过RetryerBuilder这个工厂类进行构造。RetryerBuilder负责将定义的重试策略赋值到Retryer对象中。
在Retryer执行call方法的时候,会将这些重试策略一一使用。
下面就看一下Retryer的call方法的具体实现。
/**
* Executes the given callable. If the rejection predicate
* accepts the attempt, the stop strategy is used to decide if a new attempt
* must be made. Then the wait strategy is used to decide how much time to sleep
* and a new attempt is made.
*
* @param callable the callable task to be executed
* @return the computed result of the given callable
* @throws ExecutionException if the given callable throws an exception, and the
* rejection predicate considers the attempt as successful. The original exception
* is wrapped into an ExecutionException.
* @throws RetryException if all the attempts failed before the stop strategy decided
* to abort, or the thread was interrupted. Note that if the thread is interrupted,
* this exception is thrown and the thread's interrupt status is set.
*/
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
//说明: 根据attemptNumber进行循环——也就是重试多少次
for (int attemptNumber = 1; ; attemptNumber++) {
//说明:进入方法不等待,立即执行一次
Attempt<V> attempt;
try {
//说明:执行callable中的具体业务
//attemptTimeLimiter限制了每次尝试等待的时常
V result = attemptTimeLimiter.call(callable);
//利用调用结果构造新的attempt
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);
}
}
}
}
Retryer执行过程如下:
guava-retrying实战
由于在订单超时取消时,需要进行回查支付记录,判断用户是否真的支付过。在调用支付是可能出现异常,需要进行重试。
自定义重试阻塞策略
默认的阻塞策略是线程休眠,这里使用自旋锁实现,不阻塞线程。
@Slf4j
@NoArgsConstructor
@Component
public class SpinBlockStrategy implements BlockStrategy {
@Override
public void block(long sleepTime) {
LocalDateTime startTime = LocalDateTime.now();
long start = System.currentTimeMillis();
long end = start;
log.info("线程自旋开始");
while (end - start <= sleepTime) {
end = System.currentTimeMillis();
}
//使用Java8新增的Duration计算时间间隔
Duration duration = Duration.between(startTime, LocalDateTime.now());
log.info("线程自旋结束 时间间隔={}", duration.toMillis());
}
}
自定义重试监听器
RetryListener可以监控多次重试过程,并可以使用attempt做一些额外的事情。
@Slf4j
@Component
public class RetryLogListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("重试监听开始");
// 第几次重试,(注意:第一次重试其实是第一次调用) 距离第一次重试的延迟 重试结果: 是异常终止, 还是正常返回
log.info("重试次数: [{}] 重试延迟 : [{}] 是否存在异常={} 是否返回结果={}",
attempt.getAttemptNumber(), attempt.getDelaySinceFirstAttempt()
, attempt.hasException(), attempt.hasResult());
// 是什么原因导致异常
if (attempt.hasException()) {
log.info("异常信息={}" , attempt.getExceptionCause().toString());
} else {
// 正常返回时的结果
log.info("返回结果={}" , attempt.getResult());
}
log.info("重试监听结束");
}
}
自定义Exception
有些异常需要重试,有些不需要。
public class NeedRetryException extends Exception {
public NeedRetryException(String message) {
super("远程服务调用异常,进行重试," + message);
}
}
动态调节重试策略
引入apollo动态调节重试策略。
@Slf4j
@Component
public class IndexWaitStrategy implements WaitStrategy {
@Autowired
private AppConfig appConfig;
@Override
public long computeSleepTime(Attempt failedAttempt) {
//当前重试次数
long number = failedAttempt.getAttemptNumber();
log.info("重试时间:{}", number * appConfig.getRetryDelay());
return number * appConfig.getRetryDelay();
}
}
构造重试器Retryer
将上面的实现作为参数,构造Retryer。并使用Retryer进行接口重试。
@Slf4j
@Service
public class OrderRetryService {
@Autowired
private RecordFeign recordFeign;
@Autowired
private SpinBlockStrategy spinBlockStrategy;
@Autowired
private RetryLogListener retryLogListener;
@Autowired
private IndexWaitStrategy indexWaitStrategy;
@Autowired
private AppConfig appConfig;
/**
* 获取支付记录信息重试
*/
private Retryer<TradeRecordDTO> tradeRetryer;
@PostConstruct
private void init() {
tradeRetryer = RetryerBuilder.<TradeRecordDTO>newBuilder()
//重试条件
.retryIfExceptionOfType(NeedRetryException.class)
.retryIfResult(Predicates.isNull())
//等待策略:每次请求间隔1s
//.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
//自定义等待策略:根据配置递增
.withWaitStrategy(indexWaitStrategy)
//停止策略 : 尝试请求3次
.withStopStrategy(StopStrategies.stopAfterAttempt(appConfig.getRetryTime()))
//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
//.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(appConfig.getRetryTimeLimit(), TimeUnit.SECONDS))
//默认的阻塞策略:线程睡眠
//.withBlockStrategy(BlockStrategies.threadSleepStrategy())
//自定义阻塞策略:自旋锁
.withBlockStrategy(spinBlockStrategy)
//自定义重试监听器
.withRetryListener(retryLogListener)
.build();
}
/**
* @Author xyhua
* @Description 查询支付单信息
* @Date 12:58 2020-04-08
* @param
* @return
**/
public TradeRecordDTO checkTradeRecord(String orderSn) throws ExecutionException, RetryException {
Callable<TradeRecordDTO> recordDTOCallable = () -> {
log.info("checkTradeRecord|查询支付单状态 orderSn:{}", orderSn);
TradeRecordDTO tradeRecordDTO;
try {
tradeRecordDTO = recordFeign.getTradeRecordDTOByOrderCode(orderSn).checkResult();
log.info("checkTradeRecord|查询支付单状态结果 tradeRecordDTO:{}", tradeRecordDTO);
} catch (Exception e) {
throw new NeedRetryException("订单号:" + orderSn);
}
return tradeRecordDTO;
};
return tradeRetryer.call(recordDTOCallable);
}
}
使用中遇到的问题
Guava版本过高会让AttemptTimeLimiters.fixedTimeLimit(appConfig.getRetryTimeLimit(), TimeUnit.SECONDS)
无法使用,通过降低Guava版本可以解决。