夜雨聆风学习资料网

ARTICLE · 1069856

Spring Boot 处理超大 Excel 导入:从 OOM 到 2 秒的实战优化指南

Spring Boot 处理超大 Excel 导入:从 OOM 到 2 秒的实战优化指南

百万级数据导入不再靠加内存,三个层面的优化策略让效率提升近百倍

在企业级应用开发中,Excel 导入几乎是绕不开的需求。但当我们面对几十万甚至上百万行的 Excel 文件时,传统处理方式往往会遇到两个致命问题:内存溢出(OOM) 和 性能瓶颈。一个 100MB+ 的 Excel 文件,用 XSSFWorkbook 加载,仅字符串缓存就可能占用超过 2GB 的堆内存,服务直接假死

本文将从技术选型、内存优化、并发加速三个层面,给出一套完整的 Spring Boot 大文件导入解决方案。

一、为什么传统 POI 扛不住大文件?

Apache POI 的 XSSFWorkbook 采用 DOM 模式解析,会把整个 Excel 的 XML 结构一次性解压并构建成完整的对象树,所有单元格、样式、公式都常驻内存。对于 50 万行 × 120 列的文件,光是字符串缓存(UTF-16 编码)就可能超过 300MB,加上 POI 自身的对象开销,默认 JVM 堆(通常 512MB)瞬间被击穿。

典型症状java.lang.OutOfMemoryError: Java heap space,GC 频繁,服务无响应。

二、技术选型:EasyExcel 流式读取

解决这个问题的核心思路是 流式解析——不把整个文件加载到内存,而是一行一行地读、处理完就释放。

EasyExcel 是目前最成熟的选择。它基于 SAX 模式解析 Excel,内存占用与数据量无关,只取决于批处理大小

依赖引入很简单:

<dependency>    <groupId>com.alibaba</groupId>    <artifactId>easyexcel</artifactId>    <version>3.3.2</version></dependency>

三、核心模式:监听器 + 分批入库

EasyExcel 的使用核心是一个 ReadListener,它会在每读取一行数据时触发 invoke 方法,在所有数据读取完成后触发 doAfterAllAnalysed

关键策略:不要每读一行就写一次数据库。那会产生数十万次数据库交互,性能极差。正确做法是攒够一批再批量插入。

public class StockDataListener implements ReadListener<StockData> {    private static final int BATCH_SIZE = 1000;    private List<StockData> batchData = new ArrayList<>(BATCH_SIZE);    private final StockDataMapper mapper;    @Override    public void invoke(StockData data, AnalysisContext context) {        batchData.add(data);        if (batchData.size() >= BATCH_SIZE) {            batchInsert();            batchData.clear();        }    }    @Override    public void doAfterAllAnalysed(AnalysisContext context) {        if (!batchData.isEmpty()) {            batchInsert();        }    }    @Transactional(propagation = Propagation.REQUIRES_NEW)    public void batchInsert() {        mapper.batchInsert(batchData);    }}

批次大小建议设为 1000~5000 条。太小则数据库交互频繁,太大则内存占用回升

四、进阶优化:多线程并发读取

当单线程读取成为瓶颈时,可以通过 多 Sheet 并发 进一步提速。将百万级数据分散到同一 Excel 的不同 Sheet 中,然后用线程池并发读取每个 Sheet

@Async("excelExecutor")public void readSheet(String filePath, int sheetNo, ReadListener listener) {    EasyExcel.read(filePath, UserData.class, listener)             .sheet(sheetNo)             .doRead();}

配合 Spring 的 @Async 和自定义线程池,可以充分利用多核 CPU。有实战案例显示,通过双重异步(异步读取 + 异步批量插入),10 万行数据的导入时间从 191 秒压缩到了 2 秒

但需要注意:异步方案下数据库连接池压力会显著增加,需要同步调大连接池配置。

五、数据库批量插入的优化

读取端优化完成后,数据库写入往往是新的瓶颈。除了使用 MyBatis 的 batchInsert,还可以考虑 JdbcTemplate 批量操作 或 rewriteBatchedStatements=true(MySQL 连接参数),后者能让 JDBC 驱动把多条 INSERT 合并为一条多值语句发送,写入速度可提升数倍

另外,每批使用独立事务(REQUIRES_NEW)而非一个大事务,避免长时间锁表和 undo log 膨胀。

六、错误处理与进度反馈

大数据导入必然伴随着错误处理需求。EasyExcel 的监听器中可以捕获异常,建议策略是:单条数据校验失败记录日志并跳过,批次插入失败则记录该批次数据到失败文件,不中断整体流程

进度反馈方面,可以在监听器中维护一个计数器,通过 WebSocket 或轮询接口向前端推送进度,避免用户面对“假死”的界面

总结

优化点手段效果
内存EasyExcel 流式读取内存占用与数据量解耦
读取速度多 Sheet 并发充分利用多核
写入速度批量插入 + 独立事务减少 DB 交互次数
整体吞吐双重异步读取与写入并行

核心原则只有一条:永远不要把全量数据一次性放进内存。流式读取、分批处理、及时释放,这三个词是大文件导入优化的全部精髓。

相关学习资料