httpclient并发性能问题解决

故事

前段时间进行招聘笔试,有这么一个问题,请描述实践过程中解决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>
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sirwsl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值