故事
前段时间进行招聘笔试,有这么一个问题,请描述实践过程中解决httpclient并发性能问题的案例。然后自己之前是有遇到过,但是一直没有总结,趁此机会总结一波。
问题
请描述实践过程中解决httpclient并发性能问题的案例。并描述应用场景,问题所在,解决方案。请编写示例代码。
思路
1、使用连接池进行优化
2、并发情况改NIO非阻塞异步调用
未优化直接代码
package ordinary;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import java.io.IOException;
/**
* @author sirwsl
* @createTime 2024/4/27 17:05
* @version 1.0
* @description httpClient正常没有并发情况下使用例子
* <p>
* 该样例仅作为程序在进行使用HttpClient进行普通调用返回,不会出现并发情况下的简单调用。
* </p>
*/
public class HttpCommonUse {
private static final Logger logger = Logger.getLogger(HttpCommonUse.class);
public static void main(String[] args) throws IOException {
get("https://uapis.cn/api/weather?name=昆明市");
post("https://uapis.cn/api/weather?name=昆明市",new StringEntity(""));
}
public static void get(String url) throws IOException {
// 1. 创建HttpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
// 2. 创建GET请求方法实例
HttpGet httpGet = new HttpGet(url);
// 3. 调用HttpClient实例来执行GET请求方法,得到response
CloseableHttpResponse response = httpClient.execute(httpGet);
// 4对得到后的实例可以进行处理,例如读取回复体,读取html
HttpEntity entity = response.getEntity();
//5 执行业务
logger.info(EntityUtils.toString(entity));
// 6. 释放连接
response.close();
httpClient.close();
}
public static void post(String url,HttpEntity entityString) throws IOException {
// 1. 创建HttpClient实例
CloseableHttpClient httpclient = HttpClients.createDefault();
// 2. 创建HttpPost实例
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(entityString);
// 3. 调用HttpClient实例来执行HttpPost实例
CloseableHttpResponse response = httpclient.execute(httpPost);
// 4. 读 response
String html = EntityUtils.toString(response.getEntity());
logger.info(html);
// 5. 释放连接
response.close();
httpclient.close();
}
}
优化后代码
本次优化代码分为1-4个优化样例。请选择适合自己的方式。
httpClient 异步优化
package demo;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import static java.lang.Thread.sleep;
/**
* @author: sirwsl
* @createTime: 2024/4/27 18:12
* @version: 1.0
* @description: httpClient 异步优化
* <p>
* 该例子模拟httpClient进行异步调用,本地测试耗时:2500-2900。
* 场景:适用于在项目中对同时对第三方服务进行大批量调用,如果不进行异步处理,高并发场景下会出现程序消耗大量资源,
* 且等待时间较长。
* 程序主要采用CloseableHttpAsyncClient 对httpclient进行整体异步调用,实现NIO实现非阻塞情况。
* 通过CloseableHttpAsyncClient 异步策略对原httpClient同步进行整体改善提升效率
* </p>
*/
public class HttpClientAsync {
private static final Logger logger = Logger.getLogger(HttpClientAsync.class);
public static void main(String[] args) throws IOException, InterruptedException{
long start = System.currentTimeMillis();
logger.info("请求开始,"+start);
CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom().build();
int sendTimes = 10;
CountDownLatch latch = new CountDownLatch(sendTimes);
// 3.发起调用
try {
// 3.0启动
httpclient.start();
// 3.1请求参数
String url = "https://uapis.cn/api/weather?name=昆明市";
for (int i = 0; i < sendTimes; i++) {
HttpGet httpget = new HttpGet(url);
httpclient.execute(httpget, new FutureCallback<HttpResponse>() {
public void failed(final Exception ex) {
latch.countDown();
logger.error(ex.getLocalizedMessage());
}
public void completed(final HttpResponse response) {
latch.countDown();
try {
sleep(100);
} catch (Exception e) {
}
}
public void cancelled() {
latch.countDown();
logger.error("取消");
}
});
}
} finally {
latch.await();
httpclient.close();
}
long end = System.currentTimeMillis();
logger.error("请求结束:"+end);
logger.error("耗时:"+(end- start));
}
}
HttpClient 并发调用
package demo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import static java.lang.Thread.sleep;
/**
* @author: sirwsl
* @createTime: 2024/4/27 17:56
* @version: 1.0
* @description: HttpClient 并发调用
* <p>
* 该例子中 以10个请求并发,本地测试耗时约在10000-13000左右
*
* 未进行任何优化,以普通方式进行调用,常见于普通项目中,不需要并发前提下可使用该方式
* 以该方式调用会出现程序执行效率低,等待时间长等等问题。主要造成该问题是由于HttpClient在进行执行时,重复创建导致,
* 一般的每次请求时会初创建一个httpclient,执行httpPost对象或者httpGet对象,然后从返回结果取出entity,最后关闭response释放链接。
* 高并发场景下会消耗大量服务器资源。
*
*
*
* </p>
*/
public class HttpClientDemo {
private static final Logger logger = Logger.getLogger(HttpClientDemo.class);
public static void main(String[] args) throws InterruptedException, ExecutionException, IOException {
long start = System.currentTimeMillis();
logger.info("请求开始,"+start);
HttpClient httpClient = HttpClients.createDefault();
String url = "https://uapis.cn/api/weather?name=昆明市";
int sendTimes = 10;
ExecutorService executorService = Executors.newFixedThreadPool(sendTimes);
List <Future<String>> futures = new ArrayList<>();
// 模拟10个httpClient秦秋
for (int j = 0; j< sendTimes; j++) {
Future<String> future = executorService.submit(() -> {
HttpGet httpGet = new HttpGet(url);
HttpResponse response = httpClient.execute(httpGet);
String responseBody = EntityUtils.toString(response.getEntity());
EntityUtils.consume(response.getEntity());
return responseBody;
});
futures.add(future);
}
for (Future<String> future : futures) {
String responseBody = future.get();
// 处理业务逻辑
sleep(100);
}
// Shutdown the executor service
executorService.shutdown();
long end = System.currentTimeMillis();
logger.error("请求结束:"+end);
logger.error("耗时:"+(end- start));
}
}
使用HttpClientBuilder 异步池化方式进行整体并发优化
package demo;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.Thread.sleep;
/**
* @author: sirwsl
* @createTime: 2024/4/27 18:54
* @version:
* @description: 使用HttpClientBuilder 异步池化方式进行整体并发优化。
* <p>
* 该例子中 以10个请求并发,本地测试耗时约在110-150左右。
* 场景:在进行某些并发程序处理时,由于第三方服务调用并为影响程序主体逻辑执行,可采用CloseableHttpClient+线程池方式进行处理。
* 例如某一并发请求调用第三方服务,但需对第三方服务数据进行落库处理,并不会对程序主体逻辑进行改动,此时可通过该方式缩短程序整体响应时间
* </p>
*/
public class HttpClientPool {
private static final Logger logger = Logger.getLogger(HttpClientPool.class);
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
logger.info("请求开始,"+start);
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 创建 HttpClient 对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 发送 HTTP 请求
String url = "https://uapis.cn/api/weather?name=昆明市";
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setMaxConnTotal(10);
httpClientBuilder.setMaxConnPerRoute(10);
CloseableHttpClient optimizedHttpClient = httpClientBuilder.build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
// 执行业务
sleep(100);
logger.info(EntityUtils.toString(entity));
// 关闭 HttpClient
optimizedHttpClient.close();
} catch (Exception e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executorService.shutdown();
long end = System.currentTimeMillis();
logger.error("请求结束:"+end);
logger.error("耗时:"+(end- start));
}
}
使用http连接池避免重复开销
package demo;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.io.IOException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.log4j.Logger;
import static java.lang.Thread.sleep;
/**
* @author: sirwsl
* @createTime: 2024/4/27 19:21
* @version: 1.0
* @description: 使用http连接池避免重复开销
* <p>
* 该例子中 以10个请求并发,本地测试耗时约在6000-6400左右。
* 场景:在并发情况下调用第三方服务,由于HttpClient不主动发起close,链接会维持一段时间
* 而该链接又没有进行复用,在维持的时间内,其他并发一进来,服务器会开启大量句炳,当超过上限时,无法建立新的连接,
* 此时查看netstart会出现大量的TCP链接处于ESTABLISHED状态。
*
* 在该例子中通过采用CloseableHttpResponse 对httpClient进行池化处理,
* 避免httpClient每次new\close的流程对JVM的内存消耗很大,有效避免请求过多句柄不够用情况
* </p>
*/
public class HttpClientPool2 {
private static final Logger logger = Logger.getLogger(demo.HttpClientPool2.class);
public static void main(String[] args) throws IOException, InterruptedException {
long start = System.currentTimeMillis();
logger.info("请求开始,"+start);
String url = "https://uapis.cn/api/weather?name=昆明市";
int sendTimes = 10;
HttpGet httpGet = new HttpGet(url);
for (int i = 0; i < sendTimes; i++){
CloseableHttpResponse response= HttpClientPool.getHttpClient().execute(httpGet);
HttpEntity entity = response.getEntity();
//logger.info(EntityUtils.toString(entity));
sleep(100);
}
long end = System.currentTimeMillis();
logger.error("请求结束:"+end);
logger.error("耗时:"+(end- start));
}
public static class HttpClientPool {
private static PoolingHttpClientConnectionManager cm = null;
static {
cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);
cm.setDefaultMaxPerRoute(10);
}
public static CloseableHttpClient getHttpClient() {
RequestConfig globalConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
CloseableHttpClient client = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(globalConfig).build();
return client;
}
}
}
其他准备
所使用的pom文件
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore-nio</artifactId>
<version>4.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>