背景
在实际工作中,往往会有批量查询或批量导出这样的需求,但是如果数据量很大,就不能简单的使用 in 查询来解决问题。常见的优化思路是分批处理(分而治之),今天我就来讲一下,基于CompletableFuture+多线程+内存分页实现的批量查询技巧。
实现思路
- 定义一个线程池,专门用来处理批量导出业务,和其它业务隔离。
- 先获取需要处理的总记录数,判断是否需要批量处理,如果不需要,则直接返回,如果需要则进行批量异步处理,批量处理过程中会使用到自定义的线程池。
- 统一收集异步返回的数据,进行下一步处理即可。
- 完毕
原料
- ThreadPoolTaskExecutor
- CompletableFutrue
实现细节
定义用于批处理的线程池 ThreadPoolTaskExecutor,也可以用 Java 原生自带的线程池 Executors
/**
* Spring 线程池配置组件
*
* @Author Zack
*/
@Configuration
@EnableAsync
public class SpringThreadPoolConfig {
/**
* 批量导出指定线程池
*
* @return
*/
@Bean("exportServiceExecutor")
public ThreadPoolTaskExecutor exportServiceExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
taskExecutor.setAllowCoreThreadTimeOut(true);
taskExecutor.setMaxPoolSize(50);
taskExecutor.setQueueCapacity(500);
taskExecutor.setKeepAliveSeconds(1000);
taskExecutor.setThreadNamePrefix("export-service-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
定义用于分页查询的方法,dataQuery 为查询对象,PagePO 为分页返回对象,Data 为数据对象
private PagePo<List<Data>> getDataPagePo(DataQuery dataQuery, Integer pageNumber, Integer pageSize) {
dataQuery.setPageNumber(pageNumber);
dataQuery.setPageSize(pageSize);
return dataService.queryDataList(dataQuery);
}
优先查出要批处理的总记录数,即将 pageNo 设置为 1,pageSize 设置为 1
...
dataQuery.setPageNumber(1);
dataQuery.setPageSize(1);
PagePo<List<Data>> pageList = getDataPagePo(dataQuery);
...
判断总记录数是否需要批处理,如果不需要,则查询数据后直接返回,如果需要,则进行批处理,MAX_BATCH_QUERY_SIZE 为是否需要批处理的阈值,我这里定义为 500
private List<Data> getExportList(DataQuery dataQuery, PagePo<List<Data>> pageList) {
log.info("pageList.getTotalSize(): {}", pageList.getTotalSize());
if (pageList.getTotalSize() == 1) {
return pageList.getBodyList();
}
if (pageList.getTotalSize() <= MAX_BATCH_QUERY_SIZE) {
return getListPagePo(dataQuery, 1, MAX_BATCH_QUERY_SIZE).getBodyList();
} else {
return getDataListByBatch(dataQuery, Long.valueOf(pageList.getTotalSize()).intValue());
}
}
核心批处理方法,先利用内存分页将需要处理的数据转换为任务,然后并行的执行这些任务,最后统一收集任务结果,CopyOnWriteArrayList 为同步容器,CompletableFuture 为 java8 引入的用于异步编程和获取处理结果的类,具体实现原理,不在本文的讨论范围
private List<Data> getDataListByBatch(DataQuery dataQuery, int totalSize) {
List<Data> dataList= new CopyOnWriteArrayList<>();
int totalPage = getTotalPage(totalSize);
List<Integer> pageNumbers = getPageNumbers(totalPage);
log.info("++++++++ getDataListByBatch() -> request: {}, totalSize: [{}], totalPage: [{}] ++++++++",
JSON.toJSONString(dataQuery), totalSize, totalPage);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
CompletableFuture[] completableFutures = pageNumbers.stream()
.map(pageNumber -> CompletableFuture
.supplyAsync(() -> {
PagePo<List<Data>> listPage = getListPagePo(dataQuery, pageNumber, MAX_BATCH_QUERY_SIZE);
return Objects.nonNull(listPage) ? listPage.getBodyList() : new ArrayList<Data>();
}, exportServiceExecutor)
.whenComplete((bodyList, throwable) -> {
if (!ObjectUtils.isEmpty(bodyList)) {
dataList.addAll(bodyList);
}
})).toArray(CompletableFuture[]::new);
CompletableFuture.allOf(completableFutures).join();
stopWatch.stop();
log.info("++++++++ getDataListByBatch() -> stopWatch: {}ms ++++++++", stopWatch.getTotalTimeMillis());
return dataList;
}
/**
* 处理总页数
*
* @param totalSize
* @return
*/
private int getTotalPage(int totalSize) {
return (totalSize % MAX_BATCH_QUERY_SIZE == 0)
? (totalSize / MAX_BATCH_QUERY_SIZE)
: (totalSize / MAX_BATCH_QUERY_SIZE + 1);
}
/**
* 获取页数
*
* @param totalPage
* @return
*/
private List<Integer> getPageNumbers(int totalPage) {
int pageNumber = 1;
List<Integer> pageNumbers = Lists.newArrayList();
while (pageNumber <= totalPage) {
pageNumbers.add(pageNumber++);
}
return pageNumbers;
}
结尾语
批处理一直是工作中最常见的问题,在实现过程中,往往需要考虑比较多的因素,但是解决的思路基本是一致的,总结了比较重要的几点:
- 在复杂业务条件下,应充分利用内存计算解决复杂场景中的SQL性能问题
- 利用批量查询替代循环查询,减少网络IO带来的损耗
- 利用内存分页+线程池+多线程并行处理,提升程序处理的性能