springboot+poi导出百万级数据避免OOM内存溢出


文章目录

  • springboot+poi导出百万级数据避免OOM内存溢出
  • 前言
  • 一、具体实现
  • 二、代码实现
  • 1.引入poi包
  • 2.功能代码
  • 总结



前言

记录下在实际项目中碰到过一个重大问题,全量导出百万级数据,由于之前在开发环境数据没有达到百万级别,因此此问题没有处理,上到测试环境后,数据量达到150万的时候,全量导出所有的数据出现内存溢出问题。

一、具体实现

解决大数据量导出问题最关键点的在于释放内存,减少内存的消耗,避免出现OOM内存溢出问题。具体的操作为使用SXSSFWorkbook来解析数据,确保每次内存中的数据量保持在1000或者100左右,这样把其他的数据从内存中写入磁盘,以牺牲磁盘空间来释放内存,避免OOM。

二、代码实现

1.引入poi包

<dependency>
      <groupId>org.apache.poi</groupId>
       <artifactId>poi-ooxml</artifactId>
       <version>5.0.0</version>
</dependency>
<dependency>
       <groupId>org.apache.poi</groupId>
       <artifactId>poi</artifactId>
       <version>5.0.0</version>
</dependency>
<dependency>
       <groupId>org.apache.poi</groupId>
       <artifactId>poi-ooxml-full</artifactId>
       <version>5.0.0</version>
</dependency>

2.功能代码

/**
     * 处理excel数据
     *
     * @param language
     * @return
     */
    @Override
    public SXSSFWorkbook makeSXSSFWorkbook() {
        Long startTime = System.currentTimeMillis();

        // 获取总数据量
        Query queryAll = new Query();
        Long allCount = mongoTemplate.count(queryAll, CdrEntity.class);
        if (allCount <= 0) {
            return null;
        }

        // 分页参数-每页支持10000条记录
        int pageSize = 10000;

        // 记录需要循环的sheet页次数
        int loopCount = 0;

        // 由于Excel的一张sheet允许的最大行数是 1048575,所以当数据量超过 1048575 行时,需要增加 sheet 页签 -> 这里取1000000条做为一个sheet页的最大值
        int sheetMax = 1000000;
        if (allCount > sheetMax) {
            loopCount = allCount.intValue() / sheetMax;
        }
        logger.info("数据量: " + allCount);

        // 这样表示SXSSFWorkbook只会保留100条数据在内存中,其它的数据都会写到磁盘里,这样的话占用的内存就会很少
        int rowAccessWindowSize = 100;
        SXSSFWorkbook workbook = new SXSSFWorkbook(rowAccessWindowSize);

        // 记录当前页的总数
        int tempMax = 0;

        // 记录当前sheet页导出的数据量
        int tgMax = 0;

        for (int m = 0; m <= loopCount; m++) {
            SXSSFSheet sheet = workbook.createSheet("sheet" + (m + 1));
            logger.info("================写入第 " + (m + 1) + " 个sheet页================");
            
			String[] headers = new String[]{"呼叫类型", "话单类型", "主叫号码"};
           

            // 设置列名
            Row row = sheet.createRow((short) 0);
            for (int i = 0; i < headers.length; i++) {
                Cell cell = row.createCell(i);
                cell.setCellValue(headers[i]);
            }

            // page:查询总数据量为 allCount条记录时,每次生成pageSize条记录  需要 page 次进行导出
            int maxAll = (int) (allCount - (m * sheetMax));
            int page = maxAll / pageSize;
            logger.info("第 " +(m + 1)+ " 个sheet页循环->page值为: " + page);

            for (int i = 0; i <= page; i++) {
                logger.info("第" + (i + 1) + "次循环开始...");
                int startIndex = i * pageSize;

                logger.info("导出分页:起始位置(startIndex) -> " + (tgMax + startIndex) + ", 每页数据量(pageSize) -> " + pageSize);

                // 查询数据
                Query queryData = new Query();
                queryData.skip((tgMax + startIndex));
                queryData.limit(pageSize);
                List<CdrEntity> dataList = mongoTemplate.find(queryData, CdrEntity.class);

                if (CollectionUtils.isNotEmpty(dataList)) {
                    for (int rowNum = 1, len = dataList.size() + 1; rowNum < len; rowNum++) {
                        Row row1 = sheet.createRow(startIndex + rowNum);
                        CdrEntity cdrEntity = dataList.get(rowNum - 1);

                        RecordVo recordVo = new RecordVo();

                        // 呼叫类型
                        recordVo.setCallType(CallTypeEnum.getNameByCode(String.valueOf(cdrEntity.getnCallType()), language));
                        Cell cell1 = row1.createCell(0);
                        cell1.setCellValue(recordVo.getCallType());

                        // 话单类型
                        recordVo.setCdrType(BillTypeEnum.getNameByCode(String.valueOf(cdrEntity.getnCdrType()), language));
                        Cell cell2 = row1.createCell(1);
                        cell2.setCellValue(recordVo.getCdrType());

                        // 主叫号码
                        recordVo.setCallerNum(cdrEntity.getStrCallerNum());
                        Cell cell3 = row1.createCell(2);
                        cell3.setCellValue(recordVo.getCallerNum());
                      
                        if (rowNum % rowAccessWindowSize == 0) {
                            try {
                                // 数据写入磁盘,释放内存
                                sheet.flushRows();
                            } catch (IOException e) {
                                logger.error("sheet从内存写入本地硬盘失败", e);
                            }
                        }
                    }
                }

                tempMax = (i + 1) * pageSize;
                // 当达到一个sheet页数据量最大值时,跳出当次循环,继续循环下一个sheet数据
                if (tempMax >= sheetMax) {
                    tgMax = (m + 1) * sheetMax;
                    logger.info("tgMax值: " + tgMax);
                    logger.info("结束第 " + (m + 1) + " 个sheet页循环...");
                    break;
                }
            }
        }
        Long endTime = System.currentTimeMillis();
        logger.info("消耗时间: " + (endTime - startTime));
        return workbook;
    }
/**
     * 导出操作
     *
     * @param request
     * @param response
     * @param workbook
     * @param language
     * @param systemType
     * @return
     */
    @Override
    public boolean exportExcel(HttpServletRequest request, HttpServletResponse response, SXSSFWorkbook workbook) {
        OutputStream out = null;
        String fileName = "test.xlsx";
        
        try {
            out = response.getOutputStream();
            response.setContentType("application/x-download");
            response.setHeader("Pragma", "public");
            response.setHeader("Cache-Control", "max-age=30");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName);
            workbook.write(out);
            out.flush();
        } catch (Exception e) {
            logger.error("导出Excel文件失败", e);
            return false;
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    logger.error("导出Excel文件关闭输出流失败", e);
                } finally {
                    out = null;
                }
            }
            if (workbook != null) {
                try {
                    workbook.dispose();
                    workbook.close();
                } catch (IOException e) {
                    logger.error("导出Excel文件关闭输出流失败", e);
                } finally {
                    workbook = null;
                }
            }
        }
        return true;
    }

总结

这段代码亲测有效,到测试环境上导出五百多万条数据都没有出现OOM问题,希望对各位朋友有效。这里实现了多个sheet页导出的数据,excel导出一个sheet最多能导出100万多一点数据,至于具体数字,请朋友们自行百度下。谢谢大家