介绍

在测试环境中,文件导出感觉不出来性能上的差异。在完成一些需求后,导师采纳了我的代码上线了生产环境。在此之前,我已经对导出功能进行了一些优化,体现在,除去了原代码中出现的循环查库,将其进行提前预处理,以及添加了可根据条件进行导出的功能(原来的代码只有全量导出)。次日,导师跟我说,导出速度慢,具体量值:3分钟导出4-5万条数据。

初步实现

导师给我的方案是:将导出数据量进行控制,只要从导出天数算起,X天的数据。在那一刻,我也并没有多去想,而是执行他给的任务。这并不困难,我只需要在对应的查询上,添加一个条件即可。项目用的是MyBatis-plus,我找到了那一个方法,添加了一个wrapper,就完成了效果。

1
wrapper.ge(A::getCreateTime, LocalDate.now().minusDays(EXPORT_DAY));

测试效果:

筛选完数据后,导出来的数据量大概在2万条左右,导出时间在2分钟左右。emmm….效果并没有很理想

img

因为本质上,这一方案并没有从技术层面上去解决!!!

分析

初步优化:先问AI

EasyExcel 是一种轻量级的用于读写 Excel 文件的库,但是在使用时可能会遇到导出速度慢的问题。以下是一些可能影响导出速度的因素:

数据量大:大量数据行和列会显著增加处理时间和内存消耗。

复杂的数据格式:如果每个单元格都需要复杂的样式设置(如字体、颜色、对齐方式等等),则会增加处理时间。

IO性能:磁盘IO速度慢或者网络磁盘访问延迟,都会影响导出的速度。

写入方式不优化:一次写入大量数据,可能导致内存占用高或者写入速度慢。

硬件性能:计算机的CPU、内存等硬件性能不够强大时,性能瓶颈也会显现。

多线程与并发:EasyExcel 本身对于复杂并发的处理不是特别优化,如果有多线程的需求,没有有效的线程管理和负载均衡,可能导致性能下降。

文件大小:Excel文件本身达到一定大小后,文件的读写性能会显著下降。

JVM参数:分配给JVM的内存不够,导致频繁的垃圾回收(GC),影响性能。

为解决这些问题,可以尝试以下优化方法:

  • 分批写入

  • 简化格式设置

  • 优化硬件

  • 调优JVM参数

  • 采用流式导出

  • 多线程处理
  • 减少文件大小
  • 使用其他工具

通过这些方法,可以在一定程度上提高EasyExcel的导出速度。

  • 分批
  • 多线程处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 大致代码 通过Gava的partition进行分组,使用 CountDownLatch 进行多线程写如不同的sheet
public void test() {
// 查询出的数据
List<String> strList = new ArrayList<>();
List<List<String>> listList = Lists.partition(strList, 5000);
String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
ExcelWriter excelWriter = EasyExcel.write(fileName).build();
CountDownLatch countDownLatch = new CountDownLatch(listList.size());
// 这里最终会写到多个sheet里面
for (int i = 0; i < listList.size(); i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class 实际上可以一直变
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(head()).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
taskExecutor.execute(() -> write(listList.get(i), countDownLatch, excelWriter, writeSheet));
}
// 千万别忘记finish 会帮忙关闭流
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
excelWriter.finish();
}


private void write(List<String> data, CountDownLatch countDownLatch, ExcelWriter excelWriter, WriteSheet writeSheet) {
excelWriter.write(data, writeSheet);
countDownLatch.countDown();
}

测试 报错了! EasyExcel并不支持多线程

1
Exception in thread "pool-2-thread-1" org.apache.poi.ooxml.POIXMLException: org.apache.poi.openxml4j.exceptions.InvalidOperationException: You can't add a part with a part name derived from another part ! [M1.11]

但是听过很多次说Easyexcel的并行导入,怎么现在实战不行了?

AI分析+博客:最终原因是单个ExcelWriter不支持多线程

持续优化

将创建ExcelWriter移入for循环

1
2
3
4
5
6
7
8
// 大致代码 通过Gava的partition进行分组,使用 CountDownLatch 进行多线程写如不同的sheet
public void test() {
for (int i = 0; i < listList.size(); i++) {
ExcelWriter excelWriter = EasyExcel.write(fileName).build();
// ......
}

}

高兴测试

发现接口还是一分多钟,优化不是很明显,都打算放弃了,看到博客写可能不是EasyExcel的问题,是代码的问题。对啊都没分析过代码的耗时,死盯着EasyExcel了。

排查

  1. 启动 arthas

根据jps找到对应的java进程选择

  1. 使用 trace 跟踪方法
1
trace 全限定类名 方法名 --skipJDKMethod false 

—skipJDKMethod让jdk自带的方法也打印出来

img

steam流的collect操作耗时严重!!!

这里是一段DTO转VO的操作

后续的修改

在查询过程中 只选择VO需要的字段 ,对于一些逻辑转换直接在SQl层面进行一些函数转换(可能不太好),删除DTO转VO的代码

这是整体导出时间:1.8min -> 16s 这个提升还是不错哒!

img