一种超大规模数据导出Excel的实现方法
在数据处理和报表生成中,将超大规模数据导出到Excel文件是一项常见且充满挑战的任务。传统的Excel处理库如Apache POI在处理大数据量时,极易因将大量数据对象堆积在内存中而导致内存溢出(OOM)。为了解决这个痛点,阿里巴巴开源了EasyExcel库,它通过流式写入和事件驱动模型,显著降低了内存占用,能够轻松应对百万级数据的导出需求。
本文将详细介绍如何使用Java结合EasyExcel,通过分页从数据库读取数据,并将数据分批流式写入Excel文件,实现高效、稳定的超大规模数据导出。
一、为什么是EasyExcel?
EasyExcel是阿里巴巴开源的一个基于Java的简单、省内存的读写Excel的库。
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
EasyExcel特别适用于处理大数据量的Excel文件,能够轻松应对百万级数据的导入导出需求。
二、设计思路:分页查询 + 分批写入
解决大数据量导出内存溢出的核心思路很简单:分页读取数据库 + 流式写入Excel,实现内存常驻对象最小化。
具体流程如下:
- 从数据库分页查询数据(如每次查询20万条)。
- 每查询到一批数据,立即使用EasyExcel写入到Excel文件流中。
- 写入完成后,释放该批数据的引用,让GC可以回收内存。
- 循环上述过程,直到所有数据导出完毕。
通过这种方式,内存中始终只保留当前分页的数据,无论总数据量多大,都不会导致内存溢出。
三、代码实现
环境要求:JDK 21 + Spring Boot 3.x
3.1 Maven依赖
首先,你需要在你的Java项目中引入EasyExcel的依赖,实际使用过程中,将version替换成最新版本。以下是一个Maven依赖示例:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.2</version>
</dependency>
3.2 动态头(Head)
动态表头适用性更广,且不需要创建POJO。
private List<List<String>> head() {
return List.of(List.of("双精度浮点数"),
List.of("整型"),
List.of("日期"));
}
3.3 模拟数据(Data)
动态获取批量数据,这为模拟数据库数据,真实场景下一般从数据库分页获取数据。
private List<List<Object>> data(int pageSize) {
List<List<Object>> result = new ArrayList<>(pageSize);
for (int i = 0; i < pageSize; i++) {
result.add(List.of(Random.from(RandomGenerator.getDefault()).nextDouble(),
Random.from(RandomGenerator.getDefault()).nextInt(),
LocalDateTime.now()));
}
return result;
}
3.4 核心逻辑:分批流式写入excel
由于数据量超大,我们需要分页读取并将数据写入excel。
接下来,我们使用EasyExcel将分页查询到的数据分批写入Excel文件的多个sheet。这里的关键是控制每次写入的数据量,避免内存溢出。
示例代码通过模拟数据写入,真实场景可以采用mybatis从数据库分页查询数据写入excel。
tips:info
在实际项目应用中,可根据数据库中的实际数据量来确定总页数total的值。
pageSize可以根据你的CPU性能和内存大小进行灵活调整,更进一步可通过配置注入,随时调整。
使用try-with-resources,它将自动关闭流并释放资源。
public void export() {
int pageNum = 0;
int pageSize = 200000;
long total = 5000000L;
// 使用 try-with-resources 确保流自动关闭并释放资源
try (ExcelWriter excelWriter = EasyExcel.write("./export.xlsx").build()) {
do {
WriteSheet writeSheet = EasyExcel.writerSheet(pageNum, "雇员页" + (pageNum + 1)).build();
writeSheet.setHead(head());
// 【真实场景】从数据库分页查询
// Page<Object> page = PageHelper.startPage(pageNum + 1, pageSize)
// .setOrderBy("id desc")
// .doSelectPage();
// total = page.getTotal();
// List<List<Object>> data = page.getResult().stream()
// .map(this::convertToRow)
// .collect(Collectors.toList());
List<List<Object>> data = data(pageSize);
excelWriter.write(data, writeSheet);
pageNum++;
} while ((pageNum + 1L) * pageSize < total);
}
}
关键参数说明
| 参数 | 说明 |
|---|---|
pageSize |
每批查询的数据量,建议根据数据库查询性能和JVM内存大小综合评估。20万是一个经验值,可通过配置注入动态调整 |
total |
数据总量,用于控制循环终止条件。真实场景从数据库查询获取 |
ExcelWriter |
使用 try-with-resources 管理,确保资源释放 |
3.5 生产环境增强:异步导出 + 超时处理
在Web应用中,如果同步导出百万级数据,接口很可能因为执行时间过长而超时。生产环境建议采用异步导出方案。
方案一:异步任务 + 下载链接
用户提交导出请求后立即返回任务ID,前端通过轮询或WebSocket查询任务状态,完成后展示下载链接。这种方式彻底解决了HTTP超时问题,用户体验更好。
方案二:ResponseEntity + 流式下载
如果数据量在可控范围内(如百万级以内),也可以直接将文件流写入 HttpServletResponse:
四、性能优化建议
| 优化点 | 建议 |
|---|---|
| JVM内存 | 根据数据量设置合理的堆内存,如 -Xms1g -Xmx1g |
| 关闭自动列宽 | 自动列宽会遍历所有数据计算宽度,大数据量时非常耗时,建议手动设置固定列宽 |
| 减少对象复杂度 | 导出数据对象字段越少,内存占用越低,序列化开销越小 |
| 数据库查询优化 | 分页查询时使用覆盖索引,避免回表;使用流式游标(如MyBatis Cursor)进一步降低内存 |
| 异步处理 | 大数据量导出必须异步任务化,避免阻塞Web容器线程 |
| 并行导出(可选) | 可将数据按ID范围拆分成多个子任务,并行导出多个Excel文件后压缩为ZIP |
五、验证:JVM内存监控
在测试中,设置堆内存 -Xms384m -Xmx384m,导出500万条数据时,内存占用稳定在300MB以内。这验证了 “分页查询 + 分批写入” 方案在处理超大规模数据时的有效性。
实现效果

设置堆内存大小 -Xms384m -Xmx384m
JVM监控

六、总结
本文实现了一套完整的超大规模数据导出方案,核心要点如下:
- 技术选型:EasyExcel通过流式写入 + 事件驱动模型,从根本上解决了POI的内存溢出问题。
- 核心策略:分页查询 + 分批写入,确保内存中常驻对象最小化。
- 健壮性:使用
try-with-resources保证资源释放,配合手动清理和日志记录。 - 生产增强:大数据量导出必须异步化,避免HTTP超时。
通过这套方案,可以稳定地导出百万级甚至千万级数据,而无需担心内存溢出或接口超时问题。
七、扩展
动态头和动态数据结合,可将excel数据导出的功能进行高度抽象,将导出功能建成一个微服务,本例称为:ExportMicroservice。数据导出以异步任务的形式执行,用户提交一个导出请求,微服务A提交任务到ExportMicroservice,将要导出的数据通过消息中间件推送到ExportMicroservice,用户只需等待任务导出成功的消息。
该微服务可定义一种数据协议。一种可供参考的数据交换协议如下:
创建任务的http请求的数据协议如下:
{
"filename": "测试表格",
"head": [
{
"type": "double",
"display": "双精度浮点数"
},
{
"type": "int",
"display": "整型"
},
{
"type": "LocalDateTime",
"display": "日期"
},
{
"type": "URL",
"display": "日期"
}
],
"total": 100000,
"sheetName": "测试"
}
该json定义了表头,文件名,数据量等,可根据实际情况扩展。
而需导出的数据,可通过消息中间件Kafka推送给该微服务,可单条推送,也可一次性推送多条数据,Kafka的数据协议如下:
[
{
"double": -8043262919442300000,
"int": 1301170199,
"LocalDateTime": "2024/8/14 18:01:40",
"URL": "https://yanglei.ltd/archives/gEZtR6Qw"
},
{
"double": -8043262919442300000,
"int": 1301170199,
"LocalDateTime": "2024/8/14 18:01:40",
"URL": "https://yanglei.ltd/archives/gEZtR6Qw"
},
{
"double": -8043262919442300000,
"int": 1301170199,
"LocalDateTime": "2024/8/14 18:01:40",
"URL": "https://yanglei.ltd/archives/gEZtR6Qw"
}
]
定义该数据格式,主要是方便服务使用者组装数据,ExportMicroservice在收到该数据后,需要清洗为List<List
以上仅提供一种Excel导出服务的设计思路,具体实现方式可结合现实场景进行改造。
比如有些场景下不仅有结构化数据导出,还有非结构化数据导出,如图片和个人信息同时导出,这时可以通过type进行识别,若type为URL时进行图片导出,并将图片链接写入Excel的单元格,其他type时仅进行字符串导出,当然,还可以结合EasyExcel的拦截器对Excel的单元格进行一些特殊设置,如对图片链接设置蓝色并加下划线,可使导出效果更佳。