一文总结超时重试、guava-retry、spring-retry

概述

超时:在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的思想值得学习。
在这里插入图片描述

实例

需要定义实现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动作。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?

其他阅读

posted @ 2020-04-30 01:02  johnny233  阅读(67)  评论(0编辑  收藏  举报  来源