一文总结超时重试、guava-retry、spring-retry
  TEZNKK3IfmPf 2023年11月13日 15 0

超时:在HTTP请求中设置超时时间,超时后就断开连接,防止服务不可用导致请求一直阻塞,从而避免服务资源的长时间占用。
重试:一般使用在对下层服务强依赖的场景。利用重试来解决网络异常带来的请求失败的情况,超时次数不应该太多,超时时间也比较关键。通过设置请求时间和记录请求次数来判断是否需要重试即可,框架实现有guava-retry和spring-retry。

超时

一次完整的请求包括三个阶段:

  1. 建立连接
  2. 数据传输
  3. 断开连接

ConnectionTimeOut、SocketTimeOut、ConnectionRequestTimeout区别

  1. ConnectionTimeOut:连接建立时间,三次握手完成时间;与服务器请求建立连接的时间超过ConnectionTimeOut,抛出 ConnectionTimeOutException。
  2. SocketTimeOut:服务器处理数据用时,超过设置值,抛出SocketTimeOutException,即服务器响应超时,服务器没有在规定的时间内返回给客户端数据。服务器连接成功,就开始数据传输,因此SocketTimeOut 即为数据传输过程中数据包之间间隔的最大时间,故而​​SocketTimeOut > ConnectionTimeOut​
  3. 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的思想值得学习。

一文总结超时重试、guava-retry、spring-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;
}

其中:

  1. 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();
}
  1. AttemptTimeLimiter:泛型接口,表示单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
public interface AttemptTimeLimiter<V> {
V call(Callable<V> callable) throws Exception;
}
  1. StopStrategy,停止重试策略,源码:
public interface StopStrategy {
boolean shouldStop(Attempt failedAttempt);
}

停止重试策略,提供三种实现类:

  • NeverStopStrategy :不停止,用于需要一直轮训知道返回期望结果的情况;
  • StopAfterDelayStrategy :设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
  • StopAfterAttemptStrategy :设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
  1. WaitStrategy,等待策略,源码:
public interface WaitStrategy {
long computeSleepTime(Attempt failedAttempt);
}

根据失败的Attempt次数计算控制时间间隔,返回结果为下次执行时长:

  • FixedWaitStrategy:固定等待策略;
  • RandomWaitStrategy:随机等待策略(提供一个最小和最大时长,等待时长为其区间随机值)
  • IncrementingWaitStrategy:递增等待策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
  • ExponentialWaitStrategy:指数等待策略;
  • FibonacciWaitStrategy :Fibonacci 等待策略;
  • ExceptionWaitStrategy :异常时长策略;
  • CompositeWaitStrategy :复合时长策略;
  1. 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动作
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提供如下重试策略:

  • 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?

MQ的重试

​​使用Apache HttpClient 4.x进行异常重试​​重试利器之Guava-Retryer
​​spring-retry​​

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论

推荐阅读
  TEZNKK3IfmPf   2024年05月31日   30   0   0 服务器
  TEZNKK3IfmPf   2024年05月31日   52   0   0 linux服务器
  TEZNKK3IfmPf   2024年05月31日   31   0   0 linux服务器centos
  TEZNKK3IfmPf   2024年05月31日   37   0   0 服务器http
TEZNKK3IfmPf