一文总结超时重试、guava-retry、spring-retry
概述
超时:在HTTP请求中设置超时时间,超时后就断开连接,防止服务不可用导致请求一直阻塞,从而避免服务资源的长时间占用。
重试:一般使用在对下层服务强依赖的场景。利用重试来解决网络异常带来的请求失败的情况,超时次数不应该太多,超时时间也比较关键。通过设置请求时间和记录请求次数来判断是否需要重试即可,框架实现有guava-retry和spring-retry。
超时
一次完整的请求包括三个阶段:
- 建立连接
- 数据传输
- 断开连接
ConnectionTimeOut、SocketTimeOut、ConnectionRequestTimeout区别
- ConnectionTimeOut:连接建立时间,三次握手完成时间;与服务器请求建立连接的时间超过ConnectionTimeOut,抛出 ConnectionTimeOutException。
- SocketTimeOut:服务器处理数据用时,超过设置值,抛出SocketTimeOutException,即服务器响应超时,服务器没有在规定的时间内返回给客户端数据。服务器连接成功,就开始数据传输,因此SocketTimeOut 即为数据传输过程中数据包之间间隔的最大时间,故而
SocketTimeOut > ConnectionTimeOut
- ConnectionRequestTimeout:httpClient使用连接池来管理连接,从连接池获取连接的超时时间
重试
重试主要包括重试策略和重试次数。
重试策略:
重试次数:
重试框架
Apache httpClient
在进行http请求时,难免会遇到请求失败的情况,失败后需要重新请求,尝试再次获取数据。Apache的HttpClient提供异常重试机制,可以很灵活的定义在哪些异常情况下进行重试。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
重试前提:被请求的方法必须是幂等的:就是多次请求服务端结果应该是准确且一致的。
HttpRequestRetryHandler接口源码:
public interface HttpRequestRetryHandler {
boolean retryRequest(IOException var1, int var2, HttpContext var3);
}
实现方式:实现接口HttpRequestRetryHandler并重写retryRequest()
方法,然后通过HttpClientBuilder.setRetryHandler().build()
设置到HttpClient的构造器中即可。HttpClient提供StandardHttpRequestRetryHandler和DefaultHttpRequestRetryHandler 两个实现类,前者继承自继承自后者,并指明HTTP幂等方法的6种情况,源码:
public class StandardHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
private final Map<String, Boolean> idempotentMethods;
public StandardHttpRequestRetryHandler(int retryCount, boolean requestSentRetryEnabled) {
super(retryCount, requestSentRetryEnabled);
this.idempotentMethods = new ConcurrentHashMap();
this.idempotentMethods.put("GET", Boolean.TRUE);
this.idempotentMethods.put("HEAD", Boolean.TRUE);
this.idempotentMethods.put("PUT", Boolean.TRUE);
this.idempotentMethods.put("DELETE", Boolean.TRUE);
this.idempotentMethods.put("OPTIONS", Boolean.TRUE);
this.idempotentMethods.put("TRACE", Boolean.TRUE);
}
public StandardHttpRequestRetryHandler() {
this(3, false);
}
protected boolean handleAsIdempotent(HttpRequest request) {
String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
Boolean b = (Boolean)this.idempotentMethods.get(method);
return b != null && b;
}
}
实例:
// 省略import
public class HttpPostUtils {
public String retryPostJson(String uri, String json, int retryCount, int connectTimeout,
int connectionRequestTimeout, int socketTimeout) throws IOException, ParseException {
if (StringUtils.isAnyBlank(uri, json)) {
return null;
}
HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
if (executionCount > retryCount) {
// Do not retry if over max retry count
return false;
}
if (exception instanceof InterruptedIOException) {
// An input or output transfer has been terminated
return false;
}
if (exception instanceof UnknownHostException) {
// Unknown host 修改代码让不识别主机时重试,实际业务当不识别的时候不应该重试,再次为了演示重试过程,执行会显示retryCount次下面的输出
System.out.println("unknown host");
return true;
}
if (exception instanceof ConnectException) {
// Connection refused
return false;
}
if (exception instanceof SSLException) {
// SSL handshake exception
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
// Retry if the request is considered idempotent
return true;
}
return false;
}
};
CloseableHttpClient client = HttpClients.custom().setRetryHandler(httpRequestRetryHandler).build();
HttpPost post = new HttpPost(uri);
// Create request data
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
// Set request body
post.setEntity(entity);
RequestConfig config = RequestConfig.custom().setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout).setSocketTimeout(socketTimeout).build();
post.setConfig(config);
String responseContent = null;
CloseableHttpResponse response = null;
try {
response = client.execute(post, HttpClientContext.create());
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8.name());
}
} finally {
// close response/client in order
}
return responseContent;
}
}
在实现的 retryRequest 方法中,遇到不识别主机异常,返回 true ,请求将重试。最多重试请求retryCount次。
guava retry
guava-retry 官方:
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.
引用最新版依赖:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
此版本于Jul 1, 2015发布,整个包不过几个类,可谓是短小精悍。但是此后不再更新,故而更推荐使用spring-retry,不过guava-retry的思想值得学习。
实例
需要定义实现Callable接口的方法,以便Guava retryer能够调用。
如果抛出 IOException 则重试,如果返回结果为 null 或者等于 2 则重试,固定等待时长为 300 ms,最多尝试 3 次;
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 2;
}
};
Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
.retryIfResult(Predicates.<Integer>isNull())
.retryIfResult(Predicates.equalTo(2))
// 严格匹配Exception类型
.retryIfExceptionOfType(IOException.class)
// runtime&checked异常时都会重试, error不重试
//.retryIfException()
// 重试次数
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.withWaitStrategy(WaitStrategies.fixedWait(300, TimeUnit.MILLISECONDS))
.build();
try {
retryer.call(task);
} catch (ExecutionException | RetryException e) {
e.printStackTrace();
}
分析
retryIfException支持Predicates方式:
.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class), Predicates.instanceOf(IllegalStateException.class)))
以_error结尾才重试
.retryIfResult(Predicates.containsPattern("_error$"))
RetryerBuilder采用构造器模式,构造得到一个Retryer的实例。因此Retryer是理解guava-retry的核心。
Retryer的源码(省略注释):
public final class Retryer<V> {
private final StopStrategy stopStrategy;
private final WaitStrategy waitStrategy;
private final BlockStrategy blockStrategy;
private final AttemptTimeLimiter<V> attemptTimeLimiter;
private final Predicate<Attempt<V>> rejectionPredicate;
private final Collection<RetryListener> listeners;
}
其中:
- Attempt:泛型接口,表示一次执行任务:
public interface Attempt<V> {
V get() throws ExecutionException;
boolean hasResult();
boolean hasException();
V getResult() throws IllegalStateException;
Throwable getExceptionCause() throws IllegalStateException;
long getAttemptNumber();
long getDelaySinceFirstAttempt();
}
- AttemptTimeLimiter:泛型接口,表示单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
public interface AttemptTimeLimiter<V> {
V call(Callable<V> callable) throws Exception;
}
- StopStrategy,停止重试策略,源码:
public interface StopStrategy {
boolean shouldStop(Attempt failedAttempt);
}
停止重试策略,提供三种实现类:
- NeverStopStrategy :不停止,用于需要一直轮训知道返回期望结果的情况;
- StopAfterDelayStrategy :设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
- StopAfterAttemptStrategy :设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
- WaitStrategy,等待策略,源码:
public interface WaitStrategy {
long computeSleepTime(Attempt failedAttempt);
}
根据失败的Attempt次数计算控制时间间隔,返回结果为下次执行时长:
- FixedWaitStrategy:固定等待策略;
- RandomWaitStrategy:随机等待策略(提供一个最小和最大时长,等待时长为其区间随机值)
- IncrementingWaitStrategy:递增等待策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
- ExponentialWaitStrategy:指数等待策略;
- FibonacciWaitStrategy :Fibonacci 等待策略;
- ExceptionWaitStrategy :异常时长策略;
- CompositeWaitStrategy :复合时长策略;
- BlockStrategy,任务阻塞策略,源码:
public interface BlockStrategy {
void block(long sleepTime) throws InterruptedException;
}
通俗讲,就是当前任务执行完,下次任务还没开始这段时间做什么,默认策略为BlockStrategies.THREAD_SLEEP_STRATEGY
,即Thread.sleep(sleepTime);
6. RetryListener
public interface RetryListener {
<V> void onRetry(Attempt<V> attempt);
}
如果想自定义重试监听器,实现该接口即可,可用于异步记录错误日志。每次重试之后,guava-retry会自动回调注册的监听。可以注册多个RetryListener,会按照注册顺序依次调用。
策略模式的利用。
spring retry
引入依赖,本文以1.2.5-RELEASE源码进行讲解。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.5-RELEASE</version>
</dependency>
public interface RetryOperations {
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) throws E, ExhaustedRetryException;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState retryState) throws E;
}
如果RetryCallback执行出现指定异常,并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作。RecoveryCallback定义恢复操作,如返回mock数据或托底逻辑。
RetryTemplate实现RetryOperations,并提供线程安全的模板实现方法
RetryState用于定义有状态的重试。
@EnableRetry:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {
/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies.
*
* @return whether to proxy or not to proxy the class
*/
boolean proxyTargetClass() default false;
}
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Retryable注解
public @interface Retryable {
int maxAttemps() default 0;
}
@Recover
RetryPolicy
public interface RetryPolicy extends Serializable {
boolean canRetry(RetryContext context);
RetryContext open(RetryContext parent);
void close(RetryContext context);
void registerThrowable(RetryContext context, Throwable throwable);
}
当前版本spring-retry提供如下重试策略:
- NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试;
- AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环;
- SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略;
- TimeoutRetryPolicy:超时重试策略,默认超时时间为1秒,在指定的超时时间内允许重试;
- CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate;
- CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行。
- ExpressionRetryPolicy
- InterceptorRetryPolicy
- ExceptionClassifierRetryPolicy
BackOffPolicy
public interface BackOffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;
}
start方法会每调用一次excute(RetryCallback callbace)
时执行一次,backOff会在两次重试的间隔间执行,即每次重试期间执行一次且最后一次重试后不再执行。
当前版本spring-retry提供如下回退策略:
- NoBackOffPolicy:无退避算法策略,即当重试时是立即重试;
- FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒;
- UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在
[minBackOffPeriod,maxBackOffPeriod]
之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒; - ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier;
- ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。
- SleepingBackOffPolicy
- StatelessBackOffPolicy
有状态or无状态
无状态重试,是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中,在一次调用中进行完整的重试策略判断。
如远程调用某个查询方法时是最常见的无状态重试。
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
template.setThrowLastExceptionOnExhausted(true);
return template;
}
RetryTemplate template = new RetryTemplate();
//重试策略:次数重试策略
RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
template.setRetryPolicy(retryPolicy);
//退避策略:指数退避策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(100);
backOffPolicy.setMaxInterval(3000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setSleeper(new ThreadWaitSleeper());
template.setBackOffPolicy(backOffPolicy);
// 当重试失败后,抛出异常
String result = template.execute(new RetryCallback<String, RuntimeException>() {
@Override
public String doWithRetry(RetryContext context) throws RuntimeException {
throw new RuntimeException("timeout");
}
});
// 当重试失败后,执行RecoveryCallback
String result = template.execute(new RetryCallback<String, RuntimeException>() {
@Override
public String doWithRetry(RetryContext context) throws RuntimeException {
System.out.println("retry count:" + context.getRetryCount());
throw new RuntimeException("timeout");
}
}, new RecoveryCallback<String>() {
@Override
public String recover(RetryContext context) throws Exception {
return "default";
}
});
有状态重试,有两种情况需要使用有状态重试,事务操作需要回滚或者熔断器模式。
事务操作需要回滚场景时,当整个操作中抛出的是数据库异常DataAccessException,则不能进行重试需要回滚,而抛出其他异常则可以进行重试,可以通过RetryState实现:
//当前状态的名称,当把状态放入缓存时,通过该key查询获取
Object key = "mykey";
//是否每次都重新生成上下文还是从缓存中查询,即全局模式(如熔断器策略时从缓存中查询)
boolean isForceRefresh = true;
//对DataAccessException进行回滚
BinaryExceptionClassifier rollbackClassifier =
new BinaryExceptionClassifier(Collections.<Class<? extends Throwable>>singleton(DataAccessException.class));
RetryState state = new DefaultRetryState(key, isForceRefresh, rollbackClassifier);
String result = template.execute(new RetryCallback<String, RuntimeException>() {
@Override
public String doWithRetry(RetryContext context) throws RuntimeException {
System.out.println("retry count:" + context.getRetryCount());
throw new TypeMismatchDataAccessException("");
}
}, new RecoveryCallback<String>() {
@Override
public String recover(RetryContext context) throws Exception {
return "default";
}
}, state);
Dubbo
feign retry
batchRetryTemplate?
spring batch?