一种超大规模数据导出Excel的实现方法

一种超大规模数据导出Excel的实现方法

_

一种超大规模数据导出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,实现内存常驻对象最小化

具体流程如下:

  1. 从数据库分页查询数据(如每次查询20万条)。
  2. 每查询到一批数据,立即使用EasyExcel写入到Excel文件流中。
  3. 写入完成后,释放该批数据的引用,让GC可以回收内存。
  4. 循环上述过程,直到所有数据导出完毕。

通过这种方式,内存中始终只保留当前分页的数据,无论总数据量多大,都不会导致内存溢出。

三、代码实现

环境要求: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以内。这验证了 “分页查询 + 分批写入” 方案在处理超大规模数据时的有效性。

实现效果

excel_export.png

设置堆内存大小 -Xms384m -Xmx384m

JVM监控

export_jvm.png

六、总结

本文实现了一套完整的超大规模数据导出方案,核心要点如下:

  1. 技术选型:EasyExcel通过流式写入 + 事件驱动模型,从根本上解决了POI的内存溢出问题。
  2. 核心策略分页查询 + 分批写入,确保内存中常驻对象最小化。
  3. 健壮性:使用 try-with-resources 保证资源释放,配合手动清理和日志记录。
  4. 生产增强:大数据量导出必须异步化,避免HTTP超时。

通过这套方案,可以稳定地导出百万级甚至千万级数据,而无需担心内存溢出或接口超时问题。

七、扩展

动态头和动态数据结合,可将excel数据导出的功能进行高度抽象,将导出功能建成一个微服务,本例称为:ExportMicroservice。数据导出以异步任务的形式执行,用户提交一个导出请求,微服务A提交任务到ExportMicroservice,将要导出的数据通过消息中间件推送到ExportMicroservice,用户只需等待任务导出成功的消息。

sequenceDiagram actor User as <<Brower>> <br/> User participant Frontend as <<Js>> <br/> Frontend participant Backend as <<Java>> <br/> Backend participant ExportMicroservice as <<Java>> <br/> ExportMicroservice participant MessageQueue as <<Kafka>> <br/> MessageQueue participant Database as <<Postgres>> <br/> Database autonumber User->>Frontend: 提交导出请求 activate Frontend Frontend->>Backend: 提交导出请求到微服务 activate Backend Backend-->>Frontend: res Frontend-->>User: res deactivate Frontend Backend->>Backend: 准备动态头 Backend->>ExportMicroservice: 提交导出任务到导出微服务 activate ExportMicroservice ExportMicroservice-->>Backend: res deactivate ExportMicroservice Backend->>Database: 从数据库拉取数据 activate Database Database-->>Backend: Data deactivate Database activate MessageQueue Backend->>MessageQueue: 推送Data到消息中间件 deactivate Backend activate MessageQueue ExportMicroservice->>MessageQueue: 从消息中间件拉取Data activate ExportMicroservice deactivate MessageQueue ExportMicroservice->>ExportMicroservice: 定时器检测导出任务,判断Data数量是否等于导出任务中的total ExportMicroservice->>ExportMicroservice: 执行导出任务,解析动态头和Data循环分批导出到excel deactivate 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>的形式,再使用EasyExcel导出。

以上仅提供一种Excel导出服务的设计思路,具体实现方式可结合现实场景进行改造。

比如有些场景下不仅有结构化数据导出,还有非结构化数据导出,如图片和个人信息同时导出,这时可以通过type进行识别,若type为URL时进行图片导出,并将图片链接写入Excel的单元格,其他type时仅进行字符串导出,当然,还可以结合EasyExcel的拦截器对Excel的单元格进行一些特殊设置,如对图片链接设置蓝色并加下划线,可使导出效果更佳。

一种文件分片上传的实现方法 2026-06-12
管团队这几年,我最大的教训 2026-07-01

评论区