guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器。
在高并发开发的过程中,调用接口实现相关业务逻辑是很平常的一件事情,可是往往有时候在调用接口的过程中会因为某些因素,比如网络延迟等,造成接口调用的失败,而往往有些时候数据的重要性是不允许我们调用失败就抛个异常打个日志那么简单的。当然这样写也基本是不被允许的。我们所需要的应该是尝试重新调用相关接口,几秒延迟,尝试几次,告警消息通知等等,这一系列的措施,才是符合规格的尝试。
当然,最简单粗暴的做法就是在if判断的前提下,在else中嵌套for循环,这样不是不可以,但是这样毕竟显的很low,让人感觉不是那么的舒服,那么我们想要设计一个重试的实现需要注意哪些呢?
想要尝试的实现这些所要注意的点,当然不能一码到底,肯定要尝试着设计模式来实现,比如我们通过工厂模式构造出重试器,构造出重试器的同时设置相关的参数,比如重试条件,重启条件等等,然后再通过重试器来帮助我们去重新执行相关业务。那么实现这些逻辑,可以自己手写封装,当然了我比较懒,所以这就体现出了Guava Retrying他的用途。上面所说的注意项,他已经很友好的帮我们去实现,我们所需要的就是搬轮子,快落的撸代码就可以了。
1. Maven依赖
com.github.rholder
guava-retrying
2.0.0
注意:
引用相关依赖以后,可能会出现版本不兼容而导致项目启动报错的情况,这时候我们只需要将原有的guava排除掉,或者引用相关对应的版本即可。
报错如下:
java.lang.NoSuchMethodError: com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor()Lcom/google/common/util/concurrent/ListeningExecutorService;
at org.apache.curator.framework.listen.ListenerContainer.addListener(ListenerContainer.java:41)
at com.bzn.curator.ZkOperator.getZkClient(ZkOperator.java:207)
at com.bzn.curator.ZkOperator.checkExists(ZkOperator.java:346)
at com.bzn.curator.watcher.AbstractWatcher.initListen(AbstractWatcher.java:87)
at com.bzn.web.listener.NebulaSystemInitListener.initZkWatcher(NebulaSystemInitListener.java:84)
at com.bzn.web.listener.NebulaSystemInitListener.contextInitialized(NebulaSystemInitListener.java:33)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
尝试的解决方法:
1. 排除项目中低版本的guava依赖
com.google.guava
guava
2. 声明依赖的guava版本改为19.0即可
com.google.guava
guava
19.0
2. 实现Callable
这个其实是重试的核心,实现的同时我们加入自己的业务逻辑,也就是告诉Retrying哪些业务时需要重试的。
Callable callable = new Callable() {
public Boolean call() throws Exception {
// 需要重试的业务代码
return true;
}
};
当然,我们也可以使用利用JDK8的特性,使用lamda表达式,简单粗暴的实现即可。
Callable searchResultTask = ()-> {
//do something
return true;
};
当然了,有的时候我们方法的返回值不一定是boolean类型,比如我们调用菜单接口查询菜单项的时候,查询出来的肯定是个集合,不可能是个true或者false,这时候我们只需要将返回的泛型定义成自己所需要的即可。
因为源码中的类型是自定义的,并没有做明确的限制。
3. 通过RetryerBuilder构造Retryer
public Retryer retryer(){
//定义重试机制
Retryer retry = RetryerBuilder.newBuilder()
//retryIf 重试条件
.retryIfException()
.retryIfRuntimeException()
.retryIfExceptionOfType(Exception.class)
.retryIfException(Predicates.equalTo(new Exception()))
// 如果返回参数是null需要重试
.retryIfResult(Predicates.isNull())
// 如果返回结果不是 0000 需要重试
.retryIfResult(webResponse -> !ReturnCodeEnum.SUCCESS.getCode().equals(webResponse.getCode()))
// TODO 后期配置项挪入到 apollo
//等待策略 :每次请求间隔5s
.withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
//停止策略 : 尝试请求6次
.withStopStrategy(StopStrategies.stopAfterAttempt(6))
//时间限制 : 某次请求不得超过30s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(30, TimeUnit.SECONDS))
.withRetryListener(new WebResponseListener())
.build();
return retry;
}
我只是通过自己的Demo来展示,可以根据自己的需要自己灵活配置。
4. 使用重试器执行你的业务
retryer.call(callable);
以上,就完成了重试器的简单重试逻辑,是不是很简单?
这时候,肯定大部分人都想到了,监听器!没错,框架提供了完善的机制,我们只需要实现相关接口,定义自己的相关逻辑,注入到执行器中即可。
1. 实现RetryListener接口
public class WebResponseListener implements RetryListener {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void onRetry(Attempt attempt) {
// 第几次重试,(注意:第一次重试其实是第一次调用)
logger.info("retry time : [{}]", attempt.getAttemptNumber());
// 距离第一次重试的延迟
logger.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());
if(attempt.getAttemptNumber() == 3){
if(attempt.hasException()){
//TODO 若第几次调用还是存在错误 发送告警邮件或者发送告警短信
}
}
// 重试结果: 是异常终止, 还是正常返回
logger.info("hasException={}", attempt.hasException());
logger.info("hasResult={}", attempt.hasResult());
// 是什么原因导致异常
if (attempt.hasException()) {
logger.info("causeBy={}" , attempt.getExceptionCause().toString());
} else {
// 正常返回时的结果
logger.info("result={}" , attempt.getResult());
}
logger.info("log listen over.");
}
}
2. 重试器注入具体的监听
.withRetryListener(new WebResponseListener())
以上,一套完整的简单流程就算完成。