最近公司有个项目,没有使用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();@Autowiredpublic 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 {@Overridepublic 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()方法在有异常时直接抛出,外层业务代码捕获后,根据业务判断后续处理逻辑,比如保存操作记录用于后续补偿。
本文链接:https://my.lmcjl.com/post/5601.html
展开阅读全文
4 评论