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万多一点数据,至于具体数字,请朋友们自行百度下。谢谢大家