- 前言
- WriteWorkbook对象字段解析
- 创建文件对象
- 创建行对象写入磁盘
- 测试10w条数据导出
- 结论
- 预约导出批量查询导出操作
- 多线程并发导出测试
官方使用文档:https://alibaba-easyexcel.github.io/
前言- EasyExcel怎么避免OOM的?
- 大量数据导出怎么处理?
ExcelReaderBuilder ExcelWriterBuilder WriteWorkbook 对象 --> 一个excel文件对象
相当于一个excel 通过 ExcelWriterBuilder 构建, 就是该文件的一些特性和一些基础信息(这里的不全,可以自己点进去看),后面还有继承的WriteBasicParameter 类。
public class WriteWorkbook extends WriteBasicParameter {
private ExcelTypeEnum excelType;
private Boolean autoCloseStream;
private Boolean mandatoryUseInputStream;
private String password;
private Boolean inMemory;
private Boolean writeExcelOnException;
}
创建文件对象inMemory=flase,可以看出, 默认是通过流先读到磁盘,而不是在内存处理好在返回。因此,配合数据库 流式处理或者游标 可处理大数据。
public class WorkBookUtil {
private WorkBookUtil() {}
public static void createWorkBook(WriteWorkbookHolder writeWorkbookHolder) throws IOException {
switch (writeWorkbookHolder.getExcelType()) {
case XLSX:
// 是否用临时文件流
if (writeWorkbookHolder.getTempTemplateInputStream() != null) {
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(writeWorkbookHolder.getTempTemplateInputStream());
writeWorkbookHolder.setCachedWorkbook(xssfWorkbook);
if (writeWorkbookHolder.getInMemory()) {
writeWorkbookHolder.setWorkbook(xssfWorkbook);
} else {
writeWorkbookHolder.setWorkbook(new SXSSFWorkbook(xssfWorkbook));
}
return;
}
Workbook workbook;
// 这里可以看到是否在内存中写excel。默认false,创建缓存文件,最后写入excel。
if (writeWorkbookHolder.getInMemory()) {
workbook = new XSSFWorkbook();
} else {
workbook = new SXSSFWorkbook();
}
writeWorkbookHolder.setCachedWorkbook(workbook);
writeWorkbookHolder.setWorkbook(workbook);
return;
}
}
创建行对象写入磁盘
writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); org.apache.poi.xssf.streaming.SXSSFSheet#createRow // 创建行 private int _randomAccessWindowSize = SXSSFWorkbook.DEFAULT_WINDOW_SIZE; // 默认100行后开始刷到磁盘(MySql分页读取可以大于100条进行写入) org.apache.poi.xssf.streaming.SXSSFSheet#flushRows(int) // 把treeMap第一个节点刷到磁盘(临时文件)
上面两张图得出结论: 先读取100行数据到内存, 然后后面每读一行数据刷到磁盘中。执行写入方法后不会立刻刷盘,系统会有个缓冲区,到达一定大小后才会刷入到磁盘文件中。
疑惑1:为什么要先加载100条才开始一条条的刷入磁盘?
疑惑2:为什么存储行节点的_rows使用 TreeMap数据类型,为什么不用Queue?
groupRow():将一系列行捆绑在一起,以便它们可以折叠或展开
org.apache.poi.xssf.streaming.SXSSFSheet#dispose // 关闭写流,剩余写缓存刷新到磁盘
临时文件:
如果导出完还要对表的一些数据进行处理标注的操作;EasyExcel只能从磁盘重新读取数据到内存。所以一般只能再解析读入磁盘前进行处理。一般可以用它给出的接口RowWriteHandler,SheetWriteHandler,CellWriteHandler。
com.alibaba.excel.write.executor.ExcelWriteAddExecutor#addOneRowOfDataToExcel测试10w条数据导出
- 流式查询分批导入
先查询100条 --> excel.write --> 磁盘
mysql查询花费时间 num=100000 程序执行花费时间:74280 写入磁盘花费花费时间:2406
- 一次性查询全部导出
mysql查询花费时间 num=100000 costTime=67051 程序执行花费时间:70621
- for循环( 分页查询 -> write)
程序执行花费时间:153209结论
- EasyExcel不是一次性写入内存,所以无需一次性向MySql查询全部数据到内存中。
- 不能使用 for循环 分页查询 -> write 操作。虽然内存不会爆掉,但是很慢。5w条数据,每次100条,需要查数据库500次。(使用流式查询)
- 流式查询处理:
1. 长连接:无需多次链接数据库。减少TCP链接消耗。
2. 逐条读取:读指定条数, 进行write操作写到磁盘,减少对象堆积,内存不会爆掉。
预约导出批量查询导出操作消耗内存: 流式导入 < for循环( 分页查询 -> write) < 一次性查询全部导出
消耗时间:一次性查询全部导出 < 流式导入 < for循环( 分页查询 -> write)
多线程并发导出测试思路:流式查询处理(长连接,逐条读取), 读取几条就写入磁盘,不会耗费太多内存。
流式查询处理缺点:
- 占用数据库连接,直到关闭。
- 少量数据没有一次性查询快,适合使用在预约导出。(通过MQ拿到导出消息,后台慢慢处理。)
JVM参数
-server -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms250m -Xmx250m -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -Dfile.encoding=UTF-8
- 10条线程并发 导出1w条数据。
结果: num=10000 程序执行花费时间:6490 写入磁盘花费花费时间:452 num=10000 程序执行花费时间:7128 写入磁盘花费花费时间:674 num=10000 程序执行花费时间:15926 写入磁盘花费花费时间:891 num=10000 程序执行花费时间:18550 写入磁盘花费花费时间:1106 num=10000 程序执行花费时间:29579 写入磁盘花费花费时间:1384 num=10000 程序执行花费时间:30020 写入磁盘花费花费时间:1782 num=10000 程序执行花费时间:30147 写入磁盘花费花费时间:1806 num=10000 程序执行花费时间:34828 写入磁盘花费花费时间:2038 num=10000 程序执行花费时间:35845 写入磁盘花费花费时间:2247 num=10000 程序执行花费时间:37518 写入磁盘花费花费时间:2460
- 10条线程并发导出5w数据
num=50000 程序执行花费时间:134359 写入磁盘花费花费时间:1308 num=50000 程序执行花费时间:182012 写入磁盘花费花费时间:2362 num=50000 程序执行花费时间:194460 写入磁盘花费花费时间:3428 num=50000 程序执行花费时间:212755 写入磁盘花费花费时间:4500 num=50000 程序执行花费时间:235474 写入磁盘花费花费时间:5549 num=50000 程序执行花费时间:243594 写入磁盘花费花费时间:6570 num=50000 程序执行花费时间:248091 写入磁盘花费花费时间:7576 2022-06-16 18:48:25.452 WARN 15440 --- [nio-8088-exec-4] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@4a4b85d9 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value. 2022-06-16 18:48:25.472 ERROR 15440 --- [nio-8088-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.TransientDataAccessResourceException: ### Error querying database. Cause: java.sql.SQLException: SSL peer shut down incorrectly
Possibly consider using a shorter maxLifetime value. 应该是长连接超过最长时间导致的。但是并不会出现OOM。应该是我的数据库配置太低了。
待完善…



