概述
线上系统出现文件无法成功导出,或者导出导致虚拟机崩溃等情况。为保证系统稳定和功能正常,需对导出功能做一轮整体优化,以及整理一些可进一步优化的点。
初始导出流程如下
原因分析
1、业务数据处理异常
出现比较少,测试的正常操作难以提前发现,系统运行过程中,产生了特定数据可能就会出现的bug
2、数据量大导致内存溢出
目前系统最大数据量导出为单表3百万行,60列,全部加载到内存中极易导致OOM
3、并发操作导致内存溢出
与2中类似,实质还是数据量大的问题。由于是并行处理,因此同时存在CPU瓶颈问题。
解决方案
针对上述问题,在java应用层做了一些优化措施。对于业务处理异常,跟踪log能够比较快速的定位问题和解决。本质还是数据的输入规范问题,由于产生的数据不符合预期而导致的bug,可适当增加数据输入校验,或数据库表字段约束。在此不详谈。主要讨论大数据量和并发导致的问题解决。
直接的原因是虚拟机堆无剩余空间分配给程序即将加载的全部数据
具体措施:
方案1:物理机内存足够的情况下,可适当调大最大虚拟机堆空间,如增加启动参数-Xmx100G
优点:操作简单直接,在最大数据有一定预期的时候能够应付大部分情况。
缺点:对物理机配置要求较高,超过虚拟堆最大值的数据量依然无法处理
方案2:数据化整为零,分批处理
优点:无论多大数量级数据都可以处理
缺点:需要对整个导出流程的步骤进行调整适配,存在一定复杂度
综合考虑,选择方案2,因此以数据流向来对整个流程进行梳理优化
流程环节改进
数据库到应用程序
有两种策略:
- 分页,主要采用limit方式
- 流式处理
考虑到分页在数据量较大时,后续分页查询较慢,舍弃,选择流式处理
如下使用的是应用层jdbcTemplate的流式处理方案,对结果集的逐行处理,避免内存溢出
public void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException {
query(psc, new RowCallbackHandlerResultSetExtractor(rch));
}
mybatis同样可以实现,但由于目前使用的系统使用mybatis版本较低不支持,而升级代价较大,暂未修改
应用程序到文件
文件类型
- xls是Microsoft Excel2007前excel的文件存储格式,实现原理是基于微软的ole db是微软com组件的一种实现,本质上也是一个微型数据库,由于微软的东西很多不开源,另外也已经被淘汰,了解它的细节意义不大,底层的编程都是基于微软的com组件去开发的。
- xlsx是Microsoft Excel2007后excel的文件存储格式,实现是基于openXml和zip技术。这种存储简单,安全传输方便,同时处理数据也变的简单。
- csv 我们可以理解为纯文本文件,可以被excel打开。他的格式非常简单,解析起来和解析文本文件一样。
一般业务数据导出选择xlsx
excel的sheet存在一个行数上限值,超过该值的数据需要分sheet甚至分不同excel文件导出
.xls格式excel建议:每个sheet写入60000条数据,每个excel写入300000条数据,即5个sheet
private void updateContext(EasyExportContext context) {
int fileIdx = context.rowIdx/MAX_PER_FILE;
int sheetIdx = (context.rowIdx%MAX_PER_FILE)/MAX_PER_SHEET;
if (fileIdx > context.fileIdx) {
context.excelWriter.finish();
String fileName = context.fileNameOrg+"_"+(fileIdx+1)+".xls";
context.fileList.add(fileName);
context.excelWriter = EasyExcel.write(WebConstant.THREAD_TOB_EXPORT_URL + fileName, context.clazz).build();
context.writeSheet = EasyExcel.writerSheet(0, "" + 0).build();
context.fileIdx = fileIdx;
context.sheetIdx = 0;
} else if (sheetIdx > context.sheetIdx) {
context.writeSheet = EasyExcel.writerSheet(sheetIdx, "" + sheetIdx).build();
context.sheetIdx = sheetIdx;
}
}
poi
poi是java操作excel的一个主要工具库,并在版本更新中做了许多优化,如xlsx底层使用xml存储,占用内存会比较大,在3.8版本之后,提供了SXSSFWorkbook来优化写性能。其原理是可以定义一个window size(默认100),生成Excel期间只在内存维持window size那么多的行数Row,超时window size时会把之前行Row写到一个临时文件并且remove释放掉,这样就可以达到释放内存的效果。 SXSSFSheet在创建Row时会判断并刷盘、释放超过window size的Row。
POI没有像XLSX那样对XLS的写做出性能的优化,原因是:
- 官方认为XLS的不像XLSX那样占内存
- XLS一个Sheet最多也只能有65535行数据
POI对导入分为3种模式,用户模式User Model,事件模式Event Model,还有Event User Model。
- 用户模式(User Model)就类似于dom方式的解析,是一种high level api,给人快速、方便开发用的。缺点是一次性将文件读入内存,构建一颗Dom树。并且在POI对Excel的抽象中,每一行,每一个单元格都是一个对象。当文件大,数据量多的时候对内存的占用可想而知。 用户模式就是类似用 WorkbookFactory.create(inputStream),poi 会把整个文件一次性解析,生成全部的Sheet,Row,Cell以及对象,如果导入文件数据量大的话,也很可能会导致OOM。
- 事件模式(Event Model)就是SAX解析。Event Model使用的方式是边读取边解析,并且不会将这些数据封装成Row,Cell这样的对象。而都只是普通的数字或者是字符串。并且这些解析出来的对象是不需要一直驻留在内存中,而是解析完使用后就可以回收。所以相比于User Model,Event Model更节省内存,效率也更。但是作为代价,相比User Model功能更少,门槛也要高一些。我们需要去学习Excel存储数据的各个Xml中每个标签,标签中的属性的含义,然后对解析代码进行设计。
- User Event Model也是采用流式解析,但是不同于E