官方使用文档:https://alibaba-easyexcel.github.io/
ExcelReaderBuilder ExcelWriterBuilder
WriteWorkbook 对象 --> 一个excel文件对象
相当于一个excel 通过 ExcelWriterBuilder 构建, 就是该文件的一些特性和一些基础信息(这里的不全,可以自己点进去看),后面还有继承的WriteBasicParameter 类。
public class WriteWorkbook extends WriteBasicParameter {
/**
* CSV(".csv"),
* XLS(".xls"),
* XLSX(".xlsx") 默认xlsx
*/
private ExcelTypeEnum excelType;
/**
* Default true.
*/
private Boolean autoCloseStream;
/**
* 强制使用的inputStream .Default is false
*/
private Boolean mandatoryUseInputStream;
/**
* Whether the encryption excel是否要加密, 加密需要整个读到内存进行处理,一般不用(耗费内存)
*/
private String password;
/**
* 在内存中写入excel。默认为false,创建缓存文件并最终写入excel。
*
* Comment and RichTextString are only supported in memory mode.
*/
private Boolean inMemory;
/**
* Excel也会在抛出异常的情况下编写.默认 false.
*/
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
mysql查询花费时间 num=100000 程序执行花费时间:74280
写入磁盘花费花费时间:2406
mysql查询花费时间 num=100000 costTime=67051
程序执行花费时间:70621
程序执行花费时间: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
结果:
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
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。应该提高下内存即可,否则10条线程读取大量数据会占用大量内存,CPU上下文切换也会增大负担。
待完善…