记录一次POI线上导出25万数据出现OOM

518 阅读3分钟

我们知道有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;
    }