EasyExcel 如何导出大量数据 和 并发测试大量数据导出

文章目录

  • 前言
        • WriteWorkbook对象字段解析
        • 创建文件对象
        • 创建行对象写入磁盘
        • 测试10w条数据导出
        • 结论
        • 预约导出批量查询导出操作
        • 多线程并发导出测试

官方使用文档:https://alibaba-easyexcel.github.io/

前言

  1. EasyExcel怎么避免OOM的?
  2. 大量数据导出怎么处理?

WriteWorkbook对象字段解析

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第一个节点刷到磁盘(临时文件)

EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第1张图片
EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第2张图片

上面两张图得出结论: 先读取100行数据到内存, 然后后面每读一行数据刷到磁盘中。执行写入方法后不会立刻刷盘,系统会有个缓冲区,到达一定大小后才会刷入到磁盘文件中。
疑惑1:为什么要先加载100条才开始一条条的刷入磁盘?
疑惑2:为什么存储行节点的_rows使用 TreeMap数据类型,为什么不用Queue?
groupRow():将一系列行捆绑在一起,以便它们可以折叠或展开

org.apache.poi.xssf.streaming.SXSSFSheet#dispose	// 关闭写流,剩余写缓存刷新到磁盘

临时文件:
EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第3张图片

如果导出完还要对表的一些数据进行处理标注的操作;EasyExcel只能从磁盘重新读取数据到内存。所以一般只能再解析读入磁盘前进行处理。一般可以用它给出的接口RowWriteHandler,SheetWriteHandler,CellWriteHandler。

com.alibaba.excel.write.executor.ExcelWriteAddExecutor#addOneRowOfDataToExcel

EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第4张图片

测试10w条数据导出

  1. 流式查询分批导入
    先查询100条 --> excel.write --> 磁盘
    EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第5张图片
mysql查询花费时间 num=100000   程序执行花费时间:74280
写入磁盘花费花费时间:2406
  1. 一次性查询全部导出
    EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第6张图片
mysql查询花费时间 num=100000 costTime=67051
程序执行花费时间:70621
  1. for循环( 分页查询 -> write)
    EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第7张图片
程序执行花费时间:153209

结论

  1. EasyExcel不是一次性写入内存,所以无需一次性向MySql查询全部数据到内存中。
  2. 不能使用 for循环 分页查询 -> write 操作。虽然内存不会爆掉,但是很慢。5w条数据,每次100条,需要查数据库500次。(使用流式查询)
  3. 流式查询处理:
    1. 长连接:无需多次链接数据库。减少TCP链接消耗。
    2. 逐条读取:读指定条数, 进行write操作写到磁盘,减少对象堆积,内存不会爆掉。

消耗内存: 流式导入 < for循环( 分页查询 -> write) < 一次性查询全部导出
消耗时间:一次性查询全部导出 < 流式导入 < for循环( 分页查询 -> write)

预约导出批量查询导出操作

思路:流式查询处理(长连接,逐条读取), 读取几条就写入磁盘,不会耗费太多内存。
流式查询处理缺点:

  1. 占用数据库连接,直到关闭。
  2. 少量数据没有一次性查询快,适合使用在预约导出。(通过MQ拿到导出消息,后台慢慢处理。)

多线程并发导出测试

JVM参数

-server
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=128m
-Xms250m
-Xmx250m
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
-Dfile.encoding=UTF-8
  1. 10条线程并发 导出1w条数据。
    EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第8张图片

EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第9张图片

结果:
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
  1. 10条线程并发导出5w数据
    EasyExcel 如何导出大量数据 和 并发测试大量数据导出_第10张图片
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上下文切换也会增大负担。

待完善…

你可能感兴趣的:(java,开发语言)