前言
最近有个项目在生产环境做数据导入时,发现开始执行导入任务会出现cpu狂飙的情况。几番定位查找发现是在读取excel的时候导致此问题的发生,因此在通常使用的为POI的普通读取,在遇到大数据量excel,50MB大小或数五十万行的级别的数据容易导致读取时内存溢出或者cpu飙升。需要注意,本文讨论的是针对xlsx格式的excel文件上传。
关于Excel相关技术
在Java技术生态圈中,可以进行Excel处理的主流技术包括:Apache POI,JXL,Alibaba EasyExcel等。由于JXL只支持Excel2003以下版本,所以不太常见。
Apache POI:基于DOM方式进行解析,将文件直接加载内存,所以速度较快,适合Excel文件数量不大的应用场景
Alibaba EasyExcel:采用逐行读取的解析模式,将每一行的解析结果以观察者模式通知处理(AnalyEventListener),所以比较适合数据体量较大的Excel文件解析。
问题代码
这种方式POI会把文件的所有内容都加载到内存中,读取大的excel文件时很容易占用大量内存导致oom的发生,全部文件加载如下:
/**
* POI方式读取excel
*
* @param file
*/
public static void readExcelByPoi(File file) {
long start = ();
//整个文件都一块载入
try (InputStream inp = new FileInputStream(file);
Workbook wb = (inp)) {
("==读取excel完毕,耗时:{}毫秒,", () - start);
Sheet sheet = (0);
//更新总数
("读取结束行数:" + ());
} catch (Exception e) {
();
}
}
当前引入的poi依赖
<!-- excel工具 -->
<dependency>
<groupId></groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.0</version>
</dependency>
读取50MB我本地字段不是很多50万行数据
首先在读取excel文件的断点执行之前的cpu和内存的占用分别为50%和42%,上传的excel大小为50MB,这里我就不一一带大家测试了,以上此种方式肯定是行不通的。
解决方案一:xlsx-streamer
我们采用分段缓存的方式加载数据到内存中,此种方式在创建Workbook对象时借助xlsx-streamer(StreamingReader) 来创建一个缓冲区域批量地读取文件 ,因此不会将整个文件实例化到对象当中,代码如下:
引入依赖:
<!-- excel工具 -->
<dependency>
<groupId></groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.0</version>
</dependency>
<!-- 读取大量excel数据时使用 -->
<dependency>
<groupId></groupId>
<artifactId>xlsx-streamer</artifactId>
<version>2.1.0</version>
</dependency>
示例代码:
/**
* 大批量数据读取 十万级以上
* 思路:采用分段缓存加载数据,防止出现OOM的情况
*
* @param file
* @throws Exception
*/
public static void readLagerExcel(File file) throws Exception {
InputStream inputStream = new FileInputStream(file);
long start = ();
try (Workbook workbook = ()
.rowCacheSize(10 * 10) //缓存到内存中的行数,默认是10
.bufferSize(1024 * 4) //读取资源时,缓存到内存的字节大小,默认是1024
.open(inputStream)) { //打开资源,可以是InputStream或者是File,注意:只能打开.xlsx格式的文件
Sheet sheet = (0);
("==读取excel完毕,耗时:{}毫秒,", () - start);
//遍历所有的行
for (Row row : sheet) {
("开始遍历第" + () + "行数据:");
//遍历所有的列
for (Cell cell : row) {
(() + " ");
}
(" ");
}
//总数
("读取结束行数:" + ());
}
}
加载结果
40万级别数据近花费5秒,加载是不是很快了。
百万级别,也就花费7秒
前端也还做了个测试页面如下:
Excel文件上传
解决方案二:EasyExcel
使用EasyExcel解决大文件Excel内存溢出的问题,基于POI进行封装优化,可以在不考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。
官网: /
github:/alibaba/easyexcel
引入依赖
<!--easyExcel工具-->
<dependency>
<groupId></groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.1</version>
</dependency>
示例代码
仅做简单读取示例:
/**
* EasyExcel方式读取excel
* 读取并封装为对象,ExcelData大家需要的对象
* @param file
*/
public static void readExcelByEasyExcel(File file) {
long start = ();
List<ExcelData> excelDataList = (file).head().sheet(0).doReadSync();
().forEach(x -> (()));
("==读取excel完毕,耗时:{}毫秒,", () - start);
}
/**
* EasyExcel方式读取excel
* 不指定head类
* @param file
*/
public static void readExcelByEasyExcel1(File file) {
long start = ();
List<Map<Integer, String>> listMap = (file).sheet(0).doReadSync();
().forEach(x -> ((x)));
("==读取excel完毕,耗时:{}毫秒,", () - start);
}
得出一个结论就是使用阿里EasyExcel确实方便很多,不仅支持excel,csv也可以。支持的文件类型更多些,但是第一种方式也还可以,毕竟poi我们也一直在使用。