最近公司有个项目,没有使用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 {
private Integer connectionTimeout = 6000;
private Integer socketTimeout = 600000;
private Integer connectionRequestTimeout = 60000;
private Integer retryTimes = 5;
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());
configBuilder.setSocketTimeout(httpClientConfig.getSocketTimeout());
configBuilder.setConnectionRequestTimeout(httpClientConfig.getConnectionRequestTimeout());
configBuilder.setStaleConnectionCheckEnabled(true);
requestConfig = configBuilder.build();
}
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()) {
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;
}
}
public static CloseableHttpClient getClient(boolean isPooled) {
if (isPooled) {
return HttpClients.custom().setConnectionManager(connectionManager).setRetryHandler(retryHandler).build();
}
return HttpClients.createDefault();
}
}
可以通过配置参数指定重试次数、重试间隔等。 doPost()方法在有异常时直接抛出,外层业务代码捕获后,根据业务判断后续处理逻辑,比如保存操作记录用于后续补偿。
|