文章目录
前言
最近做到一个项目,涉及到一个应用场景:对外提供一个接口,接口的内部逻辑是要调用多次第三方接口的返回数据进行组装处理,接口的响应时间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