如何优雅高效的合并HTTP并发请求的结果


前言

最近做到一个项目,涉及到一个应用场景:对外提供一个接口,接口的内部逻辑是要调用多次第三方接口的返回数据进行组装处理,接口的响应时间100ms以内。

对于这个问题,会涉及到异步调用请求,处理回调,等待-通知机制等过程。对于响应时间的要求,可以设置timeout来控制。


一、方案调研选择

1.一种是自己造轮子

  • 基本思路:自己创建线程池,使用CountDownLatch 的countDown()和await()方法实现等待通知机制,调用http同步请求即可,获取结果可以通过入参方式赋值回调使用(当然也可以用Callable代替Runnable)。

  • 主体示例代码如下:

package com.ws;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

@Slf4j
public class MyRunnable implements Runnable{
    private int id ;
    private long startDate;
    private long endDate;

    private CountDownLatch countDownLatch ;

    private Callback callback;

    public MyRunnable(CountDownLatch countDownLatch,Callback callback, long startDate, int id){
        this.id = id ;
        this.startDate = startDate;
        this.countDownLatch = countDownLatch;
        this.callback = callback;
    }

    @Override
    public void run() {
        try {
            //同步请求逻辑 用等待代替
            //http request
            //...
            if(id ==1){
                Thread.sleep(30000);
            }else{
                Thread.sleep(3000);
            }
            callback.setResult("rusult:"+id);
            endDate =System.currentTimeMillis();
            System.out.println("--------"+id+"-----"+(endDate-startDate));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
    }
}

package com.ws;

import lombok.Data;

@Data
public class Callback {
    public String result;
}
package com.ws;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
public class CountDownLatchTest {

    public static void execute(){
        try {
            ThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
            CountDownLatch countDownLatch = new CountDownLatch(2);
            log.info("--------------start---------");
            List<Callback> list =new ArrayList<>();
            for (int i = 0; i < 2; i++) {
                Callback callback =new Callback();
                MyRunnable myRunnable = new MyRunnable(countDownLatch,callback,System.currentTimeMillis(),i+1);
                executor.execute(myRunnable);
                list.add(callback);
            }
            countDownLatch.await();

            list.forEach(call ->{
                log.info(call.getResult());
            });

            executor.shutdown();
            log.info("--------------end---------");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        execute();
    }

}

2.一种选择开源框架来解决问题

由于响应时间和并发量要求高,采用okhttp框架来解决我们的问题,不考虑HttpClient和RestTemplate等其他框架。

  • 选择依据:
    • 性能上:
      允许所有同一个主机地址的请求共享同一个socket连接
      连接池减少请求延时
      透明的GZIP压缩减少响应数据的大小
      缓存响应内容,避免一些完全重复的请求
    • API使用上:
      支持阻塞式的同步请求和带回调的异步请求,很符合我的需求。

二、业务流程图

在这里插入图片描述

三、基础代码

1.OkHttp客户端配置,主要是配置连接池和Dispatcher。

@Configuration
@Slf4j
public class OkHttpConfiguration {

    @Resource
    private PropertiesConfig propertiesConfig;

    public ConnectionPool pool;

    public JSONObject getPool(){
        JSONObject jo = new JSONObject();
        jo.put("connectionCount",pool.connectionCount());
        jo.put("idleConnectionCount",pool.idleConnectionCount());
        return jo;
    }

    @Bean
    public OkHttpClient okHttpClient() {
        OKHttpConfigBo okhttpConfig = propertiesConfig.getOKHttpConfig();
        this.pool = pool();
        return new OkHttpClient.Builder()
                //.sslSocketFactory(sslSocketFactory(), x509TrustManager())
                .retryOnConnectionFailure(false)
                .connectionPool(this.pool)
                .dispatcher(dispatcher())
                .connectTimeout(okhttpConfig.getConnectTimeout()!=null?okhttpConfig.getConnectTimeout():100, TimeUnit.MILLISECONDS)
                .readTimeout(okhttpConfig.getReadTimeout()!=null?okhttpConfig.getReadTimeout():50, TimeUnit.MILLISECONDS)
                .writeTimeout(okhttpConfig.getWriteTimeout()!=null?okhttpConfig.getWriteTimeout():50,TimeUnit.MILLISECONDS)
                .build();
    }

    @Bean
    public Dispatcher dispatcher(){
        Dispatcher dispatcher = new Dispatcher();
        OKHttpConfigBo okhttpConfig = propertiesConfig.getOKHttpConfig();
        dispatcher.setMaxRequests(okhttpConfig.getMaxRequest());
        dispatcher.setMaxRequestsPerHost(okhttpConfig.getMaxRequestsPerHost());
        return dispatcher;
    }

    @Bean
    public X509TrustManager x509TrustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
            }
            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    @Bean
    public SSLSocketFactory sslSocketFactory() {
        try {
            //信任任何链接
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Create a new connection pool with tuning parameters appropriate for a single-user application.
     * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
     */
    @Bean
    public ConnectionPool pool() {
        OKHttpConfigBo okhttpConfig = propertiesConfig.getOKHttpConfig();
        Integer maxIdleConnections = okhttpConfig.getMaxIdleConnections();
        Long keepAliveDuration = okhttpConfig.getKeepAliveDuration();
        return new ConnectionPool(maxIdleConnections!=null?maxIdleConnections:200, keepAliveDuration!=null?keepAliveDuration:5, TimeUnit.MINUTES);
    }


}

2.异步请求

    public static Call asyncPost(String url, String jsonParams,HttpRequestCallback callback) {
        String responseBody = "";
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonParams);
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
        Response response = null;
        Call call = null;
        try {
            call =okHttpClient.newCall(request);
            call.enqueue(callback);
        } catch (Exception e) {
            logger.error("okhttp3 post error >> ex = {}", ExceptionUtils.getStackTrace(e));
        }
        return call;
    }

3.实现回调接口

@Data
@Slf4j
public class HttpRequestCallback implements Callback {

    public Long startTime;
    public volatile Integer RespCode;
    public String RespMessage;
    public String RespBody;
    public Long endTime;
    public Integer priority;
    public String dsp_id;
    public MediaResponse mediaResponse;
    @Override
    public void onFailure(Call call, IOException e) {
        if(e!=null){
            this.setEndTime(System.currentTimeMillis());
            this.setRespMessage("request dsp fail");
            if(e instanceof SocketTimeoutException){
                this.setRespCode(ADStatusEnum.Timeout.getCode());
            }else{
                this.setRespCode(ADStatusEnum.SEVEN.getCode());
            }
        }

        synchronized (this){
            this.notify();
        }
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        this.setRespMessage(response.message());
        if (response.isSuccessful()) {
            this.setRespBody(response.body().string());
        }
        this.setEndTime(System.currentTimeMillis());
        this.setRespCode(response.code());
        if (response != null) {
            response.close();
        }

        synchronized (this){
            this.notify();
        }
    }

4.等待通知机制

 List<HttpRequestCallback> callbacks = new ArrayList<>();
 
 //调用异步请求,讲回调函数加到callbacks,此处代码省略
 
 callbacks.forEach(call ->{
      synchronized (call){
          if(call.getRespCode()==null){
              try {
                  call.wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  });

注意:不要使用循环等待,会过量消耗CPU,如果不想使用wait()和notify(),可以试下调用返回的call,是否可以起到等待通知的等价效果。

四、配置优化

连接池参数

  • maxIdleConnections:可以复用同host的个数
  • keepAliveDuration:keep-alive(TCP连接复用时长),也就是建立连接的握手时间可以省略 单位分钟。

分发器dispatcher参数

  • maxRequest:最大请求数
  • maxRequestsPerHost:每个Host最大请求
    根据服务器的资源和业务的特点,调整这两个参数可以加大并发量

超时时间参数

  • connectTimeout:建立创建TCP连接的时间,通过连接预热可以减少这块的时间;
  • readTimeout:inputstream时间,可以理解为获取返回数据的最大时间;
  • writeTimeout:outstream时间,可以理解为写入请求数据的最大时间;

响应时间100ms,配置如下
ReadTimeout=100,writeTimeout=50;connectTimeout=60.


总结

  • 在OkHttp中分别对Request和Response进行了封装,并通过Request对象构建了Call对象,请求由分发器进行维护和分发。对于同步请求,分发器直接在调用的线程中进行请求;而对于异步请求,主要是利用线程池进行,在请求结束后对准备请求的队列进行筛选,将合适的请求放入到运行队列当中并对异步请求进行相应的回调,通知调用者。

okhttp源码地址:https://github.com/square/okhttp

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

blackoon88

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

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

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

打赏作者

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

抵扣说明:

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

余额充值