记录一下今天碰到的问题


前言

对于经常跟网络编程打交道的你来说,并不是你的每次请求,服务端都会给你想要的结果。
重试机制虽然并不能解决这种情况,但是却可以大大减少这种情况的发生。

缘由

最近几天发现系统里面的solr数据频繁的出现数据同步不及时,统计数据以及分析日志后发现

  • 近期合并了几家公司,数据量增加了1倍多,200w+ > 400w+(数据量增加后优化搜索,可以另起话题)。
  • 访问用户增加后随之网络IO不稳定(暂定)。
  • 使用的是其他同事基于solrj开发的工具包,不知是否是最优化配置。
  • 提交数据后出现异常,只是简单的捕获处理。

土办法

先简单的加上一个重试机制。

原代码

try {
inquirySolrService.commitBean(bean);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}

处理后

int retry = 3;
while (retry > 0) {
try {
inquirySolrService.commitBean(bean);
break;
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
retry --;
}
}

本来这样已经满足了,但是这种逻辑在好几个地方都有,同样的代码,想想就恶心。
想起在使用Spring RestTemplate / HttpClient的时候,可以设置一个重试的处理器。
重试机制是理解的,但是很好奇Spring到底是怎么处理的?毕竟大神们整的东西还是有学习的地方的!

对RestTemplate重试的分析

处理器分析

具体使用配置

// 使用默认的重试处理器
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));

HttpRequestRetryHandler

public interface HttpRequestRetryHandler {

boolean retryRequest(IOException exception, int executionCount, HttpContext context);

}

DefaultHttpRequestRetryHandler

public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {

public static final DefaultHttpRequestRetryHandler INSTANCE = new DefaultHttpRequestRetryHandler();

// 重试次数
private final int retryCount;

// 是否重试
private final boolean requestSentRetryEnabled;

// 这几类异常不重试
private final Set<Class<? extends IOException>> nonRetriableClasses;

protected DefaultHttpRequestRetryHandler(
final int retryCount,
final boolean requestSentRetryEnabled,
final Collection<Class<? extends IOException>> clazzes)
{

super();
this.retryCount = retryCount;
this.requestSentRetryEnabled = requestSentRetryEnabled;
this.nonRetriableClasses = new HashSet<Class<? extends IOException>>();
for (final Class<? extends IOException> clazz: clazzes) {
this.nonRetriableClasses.add(clazz);
}
}

public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
this(retryCount, requestSentRetryEnabled, Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
SSLException.class));
}

// 默认配置3次重试,但是没有开启
public DefaultHttpRequestRetryHandler() {
this(3, false);
}

// 重试处理的核心方法
@Override
public boolean retryRequest(
final IOException exception,
final int executionCount,
final HttpContext context)
{

Args.notNull(exception, "Exception parameter");
Args.notNull(context, "HTTP context");
if (executionCount > this.retryCount) {
// Do not retry if over max retry count
return false;
}
if (this.nonRetriableClasses.contains(exception.getClass())) {
return false;
} else {
for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
}
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();

if(requestIsAborted(request)){
return false;
}

if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}

if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry
return false;
}

// 其他方法略
}

分析
这个处理器的逻辑不是很复杂,配置重试次数和重试开关,某几种异常不进行重试等等,都是Http请求特有的业务处理方法。
对于排除的异常配置,很好理解,就是非常明确这几类异常没必要重试了。

底层运行逻辑

其实RestTemplate是没有重试的运行逻辑(配置httpclient的方式),底层还是httpclient。
底层有好几个地方调用了retryRequest()逻辑,重试处理逻辑大致一样,挑RetryExec这个类吧!因为它代码比较少!

public class RetryExec implements ClientExecChain {

private final Log log = LogFactory.getLog(getClass());

private final ClientExecChain requestExecutor;
private final HttpRequestRetryHandler retryHandler;

public RetryExec(
final ClientExecChain requestExecutor,
final HttpRequestRetryHandler retryHandler)
{

Args.notNull(requestExecutor, "HTTP request executor");
Args.notNull(retryHandler, "HTTP request retry handler");
this.requestExecutor = requestExecutor;
this.retryHandler = retryHandler;
}

@Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException
{

Args.notNull(route, "HTTP route");
Args.notNull(request, "HTTP request");
Args.notNull(context, "HTTP context");
final Header[] origheaders = request.getAllHeaders();
for (int execCount = 1;; execCount++) {
try {
return this.requestExecutor.execute(route, request, context, execAware);
} catch (final IOException ex) {
// 检查可确定的中断逻辑,退出
// 代码略

// 校验是否重试
if (retryHandler.retryRequest(ex, execCount, context)) {
// 记录日志和其他业务方法
} else {
// 业务处理,并抛出异常,退出
// 代码略
}
}
}
}

}

本质上还是一个递归的逻辑,方法定义一个execCount计数,正常执行完毕就跳出循环。
出现异常时,用计数器去检查是否重试和比较重试次数,不通过就抛异常跳出循环。
好吧,我也没想到其实就是这样的!
考虑到一些其他框架里面都有重试的机制,逻辑应该都差不多,差异的只是对自己特有业务规则的处理。

httpclient底层执行请求时还有几个有意思的重试。
它把建立连接和请求内容等步骤又额外拆分成了独立的几个重试。
我猜想这样的好处是避免一个步骤出错导致整个流程重复执行。

提取重试执行工具

本着重复代码不要写第二次的原则,还是硬着头皮提取了一个重试执行工具

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
* @title:RetryUtil重试执行工具类
* @author:liuxing
* @date:2015-07-08 01:55
*/

public class RetryUtil {

private static final Logger LOGGER = LoggerFactory.getLogger(RetryUtil.class);

public interface ExecuteFunction {
void execute() throws Exception;
}

/**
* 重试执行
* @param retryCount
* @param interval
* @param timeUnit
* @param throwIfFail
* @param function
* @throws Exception
*/

public static void retry(int retryCount, long interval, TimeUnit timeUnit, boolean throwIfFail, ExecuteFunction function) throws Exception {
if (function == null) {
return;
}

for (int i = 0; i < retryCount; i++) {
try {
function.execute();
break;
} catch (Exception e) {
if (i == retryCount - 1) {
if (throwIfFail) {
throw e;
} else {
break;
}
} else {
if (timeUnit != null && interval > 0L) {
try {
timeUnit.sleep(interval);
} catch (InterruptedException e1) {
LOGGER.error(e1.getMessage(), e1);
}
}
}
}
}
}

/**
* 有间隔的重试
* @param retryCount
* @param interval
* @param timeUnit
* @param handler
* @throws Exception
*/

public static void retry(int retryCount, long interval, TimeUnit timeUnit, ExecuteFunction handler) throws Exception {
retry(retryCount, interval, timeUnit, false, handler);
}

/**
* 不间隔重试
* @param retryCount
* @param function
* @throws Exception
*/

public static void retry(int retryCount, ExecuteFunction function) throws Exception {
retry(retryCount, -1, null, function);
}

}

使用重试工具执行方法,对比下来可读性好了很多

int retry = 3;
while (retry > 0) {
try {
inquirySolrService.commitBean(bean);
break;
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
retry --;
}
}
try {
RetryUtil.retry(3, 50L, TimeUnit.MILLISECONDS, () -> {
inquirySolrService.commitBean(bean);
});
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}

PS:保持好奇心,平常碰到的简单问题也记录总结一下,挺有意思的!