前因
最近在做一些业务数据大屏统计,springcloud 架构,统计代码中需要调用别的服务获取数据,在传统的同步请求下获取所有的数据需要20-30s,有时候时间更长还会导致超时,故想到了使用mq和多线程2种解决方案。
由于任务比较急,而且别的服务并不是自己项目组负责,所以使用mq的方式就显得麻烦许多,最后评估后决定使用多线程的请求方式进行请求拆解优化,将一次请求分成多次请求,每次携带的一定个数的参数,使用多线程后,请求时间由20-30s降到了5-7s。
下面讲解大概的实现流程。
实现
1、避免在service层直接定义线程池,手动配置一个线程池。
package com.xxx.xxx.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 异步线程池配置
* 可直接注入xxxExecutor,或者通过@Async("xxxExecutor")引用
*
* @author TianXieZuoMaiKong
*/
@Slf4j
@EnableAsync
@Configuration
public class ExecutorConfig implements AsyncConfigurer {
/**
* 阻塞因子
*/
private static final double BLOCKING_COEFFICIENT = 0.9;
private static final int CPU_PROCESSORS_COUNT = Runtime.getRuntime().availableProcessors();
/**
* 默认连接池
* 用于CPU密集型任务
*/
@Bean("taskExecutor")
@Override
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("thread-async-");
executor.setCorePoolSize(CPU_PROCESSORS_COUNT + 1);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
/**
* 耗时任务连接池
* 用于IO密集型任务
*/
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor timeConsumingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("thread-io-");
executor.setCorePoolSize(CPU_PROCESSORS_COUNT);
executor.setMaxPoolSize((int) (CPU_PROCESSORS_COUNT / (1 - BLOCKING_COEFFICIENT)));
executor.setQueueCapacity(200);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return ((throwable, method, objects) -> log.error("Occurring an exception during async task [{}] invocation!\n{}", method.getName(), throwable.getMessage()));
}
}
2、定义一个任务包装类,实现Callable接口并重写其call方法,用于回调接收查询返回数据。
package com.xxx.xxx.pojo;
import com.xxx.xxx.api.OrderServiceApi;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* 根据订单号获取订单信息 handler
*
* @author TianXieZuoMaiKong
*/
public class OrderInfoHandler implements Callable<List<Map<String, Object>>> {
private Map<String, Object> paramMap;
private OrderServiceApi serviceApi;
/**
* 多参构造函数
*
* @param paramMap 订单号map参数。一次查询2000个
* @param serviceApi serviceApi,这里不想再通过反射去拿了,所以干脆当参数传进来
*/
public OrderInfoHandler(Map<String, Object> paramMap, OrderServiceApi serviceApi) {
this.paramMap = paramMap;
this.serviceApi = serviceApi;
}
@Override
public List<Map<String, Object>> call() {
return serviceApi.findOrderInfo(paramMap);
}
}
3、使用while循环拆解参数个数,一次传2000个订单号查询,订单号循环传完为止。
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.xxx.xxx.api.OrderServiceApi;
/**
* 业务处理类
*
* @author TianXieZuoMaiKong
*/
@Slf4j
@AllArgsConstructor
@Service
public class DataScreenService {
private final OrderServiceApi orderServiceApi;
private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 批量查询订单信息
*
* @param orderIds 要查询的订单id
*/
private List<Map<String, Object>> getOrderInfoList(List<String> orderIds) {
List<Future<List<Map<String, Object>>>> futureList = new ArrayList<>();
int size = orderIds.size();
int orderNum = 0;
while (orderNum < size) {
// 分批查询,一次最多能查2000个元素,超过则in查询报错
int temp = Math.min((orderNum + 2000), size);
Map<String, Object> paramMap = MapBuilder.<String, Object>init("orderIds", orderIds.subList(orderNum, temp)).getMap();
OrderInfoHandler orderInfoListHandler = new OrderInfoHandler(paramMap, orderServiceApi);
// 这里存一下Future 任务,用于下面接收回调数据。调用 submit 方法,让线程池安排去执行
futureList.add(threadPoolTaskExecutor.submit(orderInfoListHandler));
log.info("while 执行orderNum:[{}] 到 temp:[{}] 的查询。", orderNum, temp);
orderNum = temp;
}
List<Map<String, Object>> orderInfoList = new ArrayList<>(size);
for (Future<List<Map<String, Object>>> f : futureList) {
List<Map<String, Object>> r = null;
try {
r = f.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
log.error("#getorderInfoListList 多线程Future执行异常:{}", e.getMessage());
} catch (TimeoutException e) {
log.error("#getorderInfoListList 多线程Future超时。线程名:[{}]", Thread.currentThread().getName());
}
log.info("## get之后拿到结果size:[{}]", r.size());
orderInfoList.addAll(r);
}
return orderInfoList;
}
}
到这里,最后返回的 orderInfoList 就是多线程请求回来的数据集合,在自己的业务逻辑里去处理这个集合就可以了。
学无止境,生生不息。