项目上有个导出 excel 场景发现很慢,怎么优化?

Sherwin.Wei Lv7

项目上有个导出 excel 场景发现很慢,怎么优化?

回答重点

1)先定位慢在哪儿?

需要定位导出 excel 慢在哪里:

  • 业务逻辑慢?
  • 数据库查询慢?
  • excel 生成慢?

2)针对性解决慢的问题

如果是业务逻辑处理很慢,则需要优化调整逻辑,比如将一些在循环内的多次 RPC 调用,变成一次(或更少)的批量 RPC 调用。

如果是数据库查询慢,则查看 SQL 是都命中索引,是否有额外的排序逻辑等等。

一般情况下导出的场景都会分页查询数据库,可以将普通分页使用的 LIMIT offset,size 变成记录上一次查询的 id 为 lastId,然后下次查询的时候带上 lastId 作为查询条件。

例如:select * from table where id > lastId + (其他过滤条件) order by id asc limit size;

因为 LIMIT offset,size 的效率比较低,例如 limit 10000000,10,则是一直扫描到 10000000 后,跳过它们,再取接下来的 10 条数据,对数据库来说这需要大量的 I/O 操作(扫描这么多行)

至于最后 excel 生成慢的场景,一般只要避免一行一行写入即可。即批量写入数据,而不是一行一行写入。

3)多线程优化

上面优化过后,如果还想缩短导出的时间,则需要进行多线程操作。

将要导出的数据进行分片,比如以”地方“为分片依据,北京的一个 sheet、上海的一个 sheet 以此类推,多线程并发处理不同 sheet 数据的获取和写入(或者多个 excel 文件都行,最后生成一个 zip 包输出就好了)。

大数据量的情况下注意使用流式导出,防止占用过多内存,POI 使用 SXSSF,EasyExcel 默认就是流式导出。

扩展知识

多线程导出代码示例

使用 EasyExcel 导出, EasyExcel 默认使用流式写入功能(SXSSF),减少内存消耗,支持大数据量导出。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

public class ExcelExportService {

private static final String EXPORT_DIR = "path/to/export/directory"; // 临时文件存储目录

//从数据库查询数据
public List<Data> getDataByPlace(String place) {
List<Data> dataList = new ArrayList<>();
...
return dataList;
}

// 数据导出:根据地方分片生成不同的 Sheet
public void exportDataForPlace(String place, String filePath) {
List<Data> dataList = getDataByPlaceFromDB(place);

// 使用 EasyExcel 写入数据到 Excel 文件的不同 Sheet
EasyExcel.write(filePath, Data.class)
.sheet(place) // 每个地方对应一个 Sheet
.doWrite(dataList);
}

// 执行导出任务,采用多线程并发
public void export() throws InterruptedException, ExecutionException {
// 1. 定义地方列表
List<String> places = Arrays.asList("北京", "上海", "广州", "深圳", "杭州");

// 2. 设置线程池来并发处理不同地方的导出任务,此为 demo,真实环境线程池需要提前定义,
// 不可每次执行都创建
ExecutorService executorService = Executors.newFixedThreadPool(places.size());
List<Future<?>> futures = new ArrayList<>();

// 3. 创建 Excel 文件路径
String filePath = EXPORT_DIR + "/places.xlsx";

// 4. 执行每个地方的导出任务
for (String place : places) {
Future<?> future = executorService.submit(() -> {
try {
exportDataForPlace(place, filePath); // 导出数据到对应的 Sheet
} catch (Exception e) {
e.printStackTrace();
}
});
futures.add(future);
}

// 5. 等待所有任务完成
for (Future<?> future : futures) {
future.get();
}

// 6. 关闭线程池
executorService.shutdown();
System.out.println("mianshiya Excel export has been completed.");
}
}
Comments