我们知道有POI和easyExcel两个工具可以进行生成Excel表格导出数据,具体的POI和easyEXcel两个工具的使用及性能的对比就不介绍了。本次线上所使用的是POI工具进行导出数据,由于数据量过大全量数据导出产生了内存的溢出,出现了OOM异常。在没有进行优化之前是使用POI的HSSF进行导数,25万数据不断的占用内存直到内存溢出,本次的线上导数也让我对导数有一定的成长,下面来看看我是怎么进行优化的。
1、 对于POI有个工具 ExcelExportUtil能够进行大数据量进行导数,先引入相关的依赖
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>3.0.1</version>
</dependency>
2、25万数据进行分片导出,每次2000一共分125批,然后不断的append
public Workbook getWorkBook(TaxInvoiceExportReqParams params){
ExportParams exportParams = new ExportParams();
long startTime = System.currentTimeMillis();
// 设置偏移量
Integer offset = 2000;
Workbook wb = null;
// 根据条件统计总数据量
Long total = taxInvoiceManagementDao.selectExportNum(params);
// 分页
params.setPageSize(offset);
// 计算分页偏移量
Long part=total%params.getPageSize()==0?total/params.getPageSize():total/params.getPageSize()+1;
LoggerUtils.info("导出数据一共{}条,共分页{}批进行查询",total,part);
for (int i = 1; i <= part; i++) {
params.setPageNum((i-1)*params.getPageSize());
List<TaxInvoiceExportRespDTO> respDTOS = selectListExportPage(params);
LoggerUtils.info("开始导第{}批数据",i);
if(!CollectionUtils.isEmpty(respDTOS)){
LoggerUtils.info("开始导第{}批数据");
// 数据解密
DecryptText(respDTOS);
// 重点:分片数据批量进行导出然后append
wb = ExcelExportUtil.exportBigExcel(exportParams,TaxInvoiceExportRespDTO.class,respDTOS);
}
LoggerUtils.info("第{}批数据执行完毕",i);
}
long endTime = System.currentTimeMillis();
LoggerUtils.info("异步批量导出Excel耗时:{}",(endTime-startTime));
return wb;
}
3、分片导出数据异步处理、由于需要返回值,这里用CompletableFuture进行异步处理
public BaseResponse export(TaxInvoiceExportReqParams params) {
BaseResponse<Object> result = new BaseResponse<>();
String exclName = "代开票列表";
try {
// 异步分片处理、获取工作薄Workbook、用到一个公共线程池commonThreadPool
CompletableFuture<Workbook> workbookCompletableFuture = CompletableFuture.supplyAsync(() -> {
return getWorkBook(params);
}, CommonThreadPoolFactory.commonThreadPool);
// 等待处理完成会进行阻塞、获取结果进行下载并发送邮件
CompletableFuture.runAsync(() ->{
Workbook workBook = null;
try {
// 获取结果
workBook = workbookCompletableFuture.get();
} catch (InterruptedException e) {
LoggerUtils.error(e.getMessage(), e);
} catch (ExecutionException e) {
LoggerUtils.error(e.getMessage(), e);
}
// 下载、发邮件
downLoadExcel(exclName,workBook);
});
} catch (Exception e) {
LoggerUtils.error(e.getMessage(), e);
}
result.setCode(0);
result.setMessage("导出数据处理中,完成后会发送邮箱,请留意");
return result;
}
通过分片、异步处理能够解决OOM问题,异步导出处理其实还可以进行优化、分片可以通过多线程进行处理的,以上便是一次POI导出数据OOM总结。
下面是全部代码:
@RequestMapping(value = "/export", method = RequestMethod.POST)
@ResponseBody
public BaseResponse<Object> selectListExport(@RequestBody TaxInvoiceExportReqParams params) {
long startTime = System.currentTimeMillis();
String userId = LoginUtils.getLoginUserId();
BaseResponse<Object> result = new BaseResponse<>();
String join = JOINER.join(LOCK, userId);
try {
LoggerUtils.info("/api/tax/invoice/list request params:{}", JSON.toJSONString(params));
//2、调用接口 进行导出
if(redisService.lock(join,1800L)){
result = taxInvoiceManagementService.export(params);
}else {
result.setCode(1);
result.setMessage(SYSTEM_CODE);
}
} catch (Exception e) {
result.setCode(Code.FAIL.getValue());
result.setMessage("接口失败");
LoggerUtils.error("/api/tax/invoice/list request params:{}", JSON.toJSONString(params), e);
}finally {
redisService.delLock(join);
}
LoggerUtils.info("/api/tax/invoice/list result :{}", JSON.toJSON(result));
LoggerUtils.info("数据导出总耗时:{}", System.currentTimeMillis() - startTime);
return result;
}
public BaseResponse export(TaxInvoiceExportReqParams params) {
BaseResponse<Object> result = new BaseResponse<>();
String exclName = "代开票列表";
try {
// 异步分片处理、获取工作薄Workbook、用到一个公共线程池commonThreadPool
CompletableFuture<Workbook> workbookCompletableFuture = CompletableFuture.supplyAsync(() -> {
return getWorkBook(params);
}, CommonThreadPoolFactory.commonThreadPool);
// 等待处理完成会进行阻塞、获取结果进行下载并发送邮件
CompletableFuture.runAsync(() ->{
Workbook workBook = null;
try {
// 获取结果
workBook = workbookCompletableFuture.get();
} catch (InterruptedException e) {
LoggerUtils.error(e.getMessage(), e);
} catch (ExecutionException e) {
LoggerUtils.error(e.getMessage(), e);
}
// 下载、发邮件
downLoadExcel(exclName,workBook);
});
} catch (Exception e) {
LoggerUtils.error(e.getMessage(), e);
}
result.setCode(0);
result.setMessage("导出数据处理中,完成后会发送邮箱,请留意");
return result;
}
public Workbook getWorkBook(TaxInvoiceExportReqParams params){
ExportParams exportParams = new ExportParams();
long startTime = System.currentTimeMillis();
// 设置偏移量
Integer offset = 2000;
Workbook wb = null;
// 根据条件统计总数据量
Long total = taxInvoiceManagementDao.selectExportNum(params);
// 分页
params.setPageSize(offset);
// 计算分页偏移量
Long part=total%params.getPageSize()==0?total/params.getPageSize():total/params.getPageSize()+1;
LoggerUtils.info("导出数据一共{}条,共分页{}批进行查询",total,part);
for (int i = 1; i <= part; i++) {
params.setPageNum((i-1)*params.getPageSize());
List<TaxInvoiceExportRespDTO> respDTOS = selectListExportPage(params);
LoggerUtils.info("开始导第{}批数据",i);
if(!CollectionUtils.isEmpty(respDTOS)){
LoggerUtils.info("开始导第{}批数据");
// 数据解密
DecryptText(respDTOS);
// 重点:分片数据批量进行导出然后append
wb = ExcelExportUtil.exportBigExcel(exportParams,TaxInvoiceExportRespDTO.class,respDTOS);
}
LoggerUtils.info("第{}批数据执行完毕",i);
}
long endTime = System.currentTimeMillis();
LoggerUtils.info("异步批量导出Excel耗时:{}",(endTime-startTime));
return wb;
}