最近公司有个项目,没有使用OpenFeign或Dubbo这种微服务远程调用框架,使用的是原生的Apache HttpClient。项目中有个定时任务,需要多次调用其他服务对结果进行汇总,为了保证汇总结果的正确性,就需要确保每次远程调用都是成功的。这个有点类似于数据库中的事务,不知道有没有专业的技术用语,这里姑且叫做远程调用事务。
那么我自己对远程调用事务的理解就是,远程调用第三方服务接口时,由于网络抖动等不稳定性,可能会导致调用异常,比如SocketTimeOut等,这时候要保证结果的正确性,就需要事务机制,要么调用都成功,要么调用都回滚。因为当前的业务中只是多次调用接口获取结果,也就是查,而不是增删改这种操作,因此这里的处理方式就简单了很多,只需要有异常时重试即可,如果某次调用重试后仍然异常,那么就记录此次定时任务失败信息到数据库,后续可以进行定时任务补偿。
因此,本文主要是记录如何自定义HttpClient重试策略。
这里先贴两个参考的文章:
- https://www.codeleading.com/article/2829663803/
- https://www.cnblogs.com/kingszelda/p/8886403.html
下面是我的代码:
@ConfigurationProperties(prefix = "http.client")
@Configuration
@Data
@ToString
public class HttpClientConfig {
/**
* 连接超时时间 http.client.connection-timeout
*/
private Integer connectionTimeout = 6000;
/**
* 请求超时时间 http.client.socket-timeout
*/
private Integer socketTimeout = 600000;
private Integer connectionRequestTimeout = 60000;
/**
* 重试次数 http.client.retry-times
*/
private Integer retryTimes = 5;
/**
* 重试间隔(s) http.client.retry-interval
*/
private Integer retryInterval = 3000;
}
@Component
@ConditionalOnBean(HttpClientConfig.class)
public class HttpClientUtil {
private static Logger logger = LoggerFactory.getLogger(HttpClient.class);
private static PoolingHttpClientConnectionManager connectionManager;
private static RequestConfig requestConfig;
private static HttpClientConfig httpClientConfig;
private static final CustomRetryHandler retryHandler = new CustomRetryHandler();
@Autowired
public HttpClientUtil(HttpClientConfig httpClientConfig) {
HttpClientUtil.httpClientConfig = httpClientConfig;
// 设置连接池
connectionManager = new PoolingHttpClientConnectionManager();
// 设置连接池大小
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(connectionManager.getMaxTotal());
RequestConfig.Builder configBuilder = RequestConfig.custom();
// 设置连接超时
configBuilder.setConnectTimeout(httpClientConfig.getConnectionTimeout());
// 设置读取超时时间为10分钟
configBuilder.setSocketTimeout(httpClientConfig.getSocketTimeout());
// 设置从连接池获取连接实例的超时
configBuilder.setConnectionRequestTimeout(httpClientConfig.getConnectionRequestTimeout());
// 在提交请求之前 测试连接是否可用
configBuilder.setStaleConnectionCheckEnabled(true);
requestConfig = configBuilder.build();
}
/**
* 发送 POST 请求(HTTP),JSON形式
* @param apiUrl
* @param json
* @return
*/
public static String doPost(String apiUrl, Object json) throws Exception {
CloseableHttpClient httpClient = getClient(true);
String httpStr = null;
HttpPost httpPost = new HttpPost(apiUrl);
CloseableHttpResponse response = null;
try {
logger.info("http客户端配置:{}", httpClientConfig.toString());
httpPost.setConfig(requestConfig);
StringEntity stringEntity = new StringEntity(json.toString(), "UTF-8");// 解决中文乱码问题
stringEntity.setContentEncoding("UTF-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
response = httpClient.execute(httpPost);
if (response != null){
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
httpStr = EntityUtils.toString(response.getEntity(),"UTF-8");
} else {
logger.error("doPost Exception,status code = {},request uri = {}", response.getStatusLine().getStatusCode(), httpPost.getURI());
}
}
} catch (IOException e) {
logger.error(LoggerHelper.message("doPost Exception"), e);
throw new Exception(e);
} finally {
if (response != null) {
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
logger.error(LoggerHelper.message("doPost Exception"), e);
}
}
}
return httpStr;
}
/**
* 自定义重试策略
*/
static class CustomRetryHandler implements HttpRequestRetryHandler {
@Override
public boolean retryRequest(IOException arg0, int retryTimes, HttpContext arg2) {
boolean retry = false;
if (retryTimes > httpClientConfig.getRetryTimes()) {//最多重试3次,如果超过3次,则不再重试,直接抛异常
retry = false;
}else if (arg0 instanceof UnknownHostException || arg0 instanceof ConnectTimeoutException
|| !(arg0 instanceof SSLException) || arg0 instanceof NoHttpResponseException) {
retry = true;
}else if (HttpClientContext.adapt(arg2).getRequest() instanceof HttpEntityEnclosingRequest){//如果是幂等请求,则直接重试
retry = true;
}
//设置重试时间间隔
if (retry) {
try {
logger.info("=== 第 {} 次重试,最多 {} 次,间隔 {} 秒 ===",retryTimes,httpClientConfig.getRetryTimes(),httpClientConfig.getRetryInterval()/1000);
Thread.sleep(httpClientConfig.getRetryInterval());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return retry;
}
}
/**
* @param isPooled 是否使用连接池
* @return
*/
public static CloseableHttpClient getClient(boolean isPooled) {
if (isPooled) {
return HttpClients.custom().setConnectionManager(connectionManager).setRetryHandler(retryHandler).build();
}
return HttpClients.createDefault();
}
}
可以通过配置参数指定重试次数、重试间隔等。
doPost()方法在有异常时直接抛出,外层业务代码捕获后,根据业务判断后续处理逻辑,比如保存操作记录用于后续补偿。