超时:在HTTP请求中设置超时时间,超时后就断开连接,防止服务不可用导致请求一直阻塞,从而避免服务资源的长时间占用。
重试:一般使用在对下层服务强依赖的场景。利用重试来解决网络异常带来的请求失败的情况,超时次数不应该太多,超时时间也比较关键。通过设置请求时间和记录请求次数来判断是否需要重试即可,框架实现有guava-retry和spring-retry。
一次完整的请求包括三个阶段:
ConnectionTimeOut、SocketTimeOut、ConnectionRequestTimeout区别
SocketTimeOut > ConnectionTimeOut
重试主要包括重试策略和重试次数。
重试策略:
重试次数:
在进行http请求时,难免会遇到请求失败的情况,失败后需要重新请求,尝试再次获取数据。Apache的HttpClient提供异常重试机制,可以很灵活的定义在哪些异常情况下进行重试。
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.5.12version>
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 官方:
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.rholdergroupId>
<artifactId>guava-retryingartifactId>
<version>2.0.0version>
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;
}
其中:
public interface Attempt<V> {
V get() throws ExecutionException;
boolean hasResult();
boolean hasException();
V getResult() throws IllegalStateException;
Throwable getExceptionCause() throws IllegalStateException;
long getAttemptNumber();
long getDelaySinceFirstAttempt();
}
public interface AttemptTimeLimiter<V> {
V call(Callable<V> callable) throws Exception;
}
public interface StopStrategy {
boolean shouldStop(Attempt failedAttempt);
}
停止重试策略,提供三种实现类:
public interface WaitStrategy {
long computeSleepTime(Attempt failedAttempt);
}
根据失败的Attempt次数计算控制时间间隔,返回结果为下次执行时长:
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,会按照注册顺序依次调用。
策略模式的利用。
引入依赖,本文以1.2.5-RELEASE源码进行讲解。
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
<version>1.2.5-RELEASEversion>
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动作
RetryTemplate实现RetryOperations,并提供线程安全的模板实现方法
RecoveryCallback定义恢复操作,如返回假数据或托底数据。
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提供如下重试策略:
BackOffPolicy
public interface BackOffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;
}
start方法会每调用一次excute(RetryCallback callbace)
时执行一次,backOff会在两次重试的间隔间执行,即每次重试期间执行一次且最后一次重试后不再执行。
当前版本spring-retry提供如下回退策略:
[minBackOffPeriod,maxBackOffPeriod]
之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒;有状态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.<? 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);
batchRetryTemplate?
spring batch?
使用Apache HttpClient 4.x进行异常重试
重试利器之Guava-Retryer
spring-retry