大数据量 Excel 导入的性能与内存优化实战

一 核心原则

  • 使用流式/事件驱动读取(如 EasyExcel、POI SAX),避免 XSSFWorkbook 一次性将整表加载进内存,内存占用可做到与文件大小基本无关。
  • 采用分批处理 + 批量写入,每批积累到一定条数(如 1000–5000)再提交入库,避免逐条插入与超大事务。
  • 引入异步任务 + 线程池,上传接口快速返回 taskId,导入在后台执行,避免阻塞 HTTP 线程。
  • 对多 Sheet 文件可按 Sheet 并发读取,配合生产者-消费者模型提升吞吐。
  • 做好数据校验与错误隔离(跳过/覆盖/报错策略)、重试机制导入回执,保证稳定性与可观测性。

二 读取与解析层优化

  • 优先选型:使用 EasyExcel ReadListenerPOI SAX 事件模型,逐行解析,内存占用稳定;避免 XSSFWorkbook/WorkbookFactory 全量加载。
  • 批处理阈值:在 Listener 中累积到 batchSize(建议 1000–3000,视单条数据大小与内存而定)就触发一次业务处理并清空缓存。
  • 多 Sheet 并发:一个文件含多 Sheet 时,可为每个 Sheet 提交一个任务并行解析,线程池大小与 Sheet 数或 CPU 核数匹配。
  • 轻量校验:在 Listener 内做必填/格式等轻校验;复杂规则与关联查询放到批处理或落库前统一处理。

三 数据库写入层优化

  • 批量插入:使用 JDBC BatchMyBatis ExecutorType.BATCH,每批提交(如 1000–5000 条),显著减少网络往返与日志开销。
  • 连接与并发:合理设置连接池大小并发线程数,避免连接耗尽与上下文切换过多。
  • 事务策略:避免“一导入一事务”的大事务,改为按批提交;对失败批次可重试 2–3 次后记录错误明细。
  • 唯一性冲突:在数据库设置唯一约束,冲突时按业务选择覆盖/跳过/报错策略。
  • 极致场景:将清洗后的数据先落 CSV/临时表,再用 LOAD DATA INFILE 或数据库原生批量导入工具,速度常优于逐条 ORM 插入。

四 架构与工程化优化

  • 异步化:上传接口立即返回 taskId,导入任务进入线程池/消息队列执行;前端轮询或 WebSocket 查询进度与结果。
  • 背压与限流:对并发导入数、单文件大小、单批次大小做限流与熔断,保护服务稳定性。
  • 错误回执与重试:导入结束后生成成功/失败明细下载;失败批次支持定位与重放
  • 监控与告警:监控 JVM GC/内存、线程池队列、数据库连接、导入耗时,异常及时告警。

五 参数与配置建议

  • 批次大小:从 2000 起步,结合单条数据体积与内存做压测,通常控制在 1000–5000 区间。
  • 并发度:多 Sheet 可按 Sheet 数并行;无 Sheet 并行时,控制读取线程:写入线程 ≈ 1:2~1:4,避免写库成为瓶颈。
  • JVM 与容器:适当增大堆内存(如 -Xmx4G/-Xmx8G),但根本仍依赖流式处理而非堆扩容。
  • 数据库:开启批处理优化(如 MySQL 的 rewriteBatchedStatements=true),合理设置 fetchSize、事务隔离级别
  • 超时与池化:调大 HTTP 超时连接池最大连接/空闲线程池队列,防止长导入被中断。

六 落地代码示例

  • 批量模式监听器(EasyExcel)
public class BatchExcelListener<T> extends AnalysisEventListener<T> {
    private final int batchSize;
    private final List<T> batch = new ArrayList<>(batchSize);
    private final Consumer<List<T>> processor;
    private final AtomicInteger total = new AtomicInteger();
    private final AtomicInteger failed = new AtomicInteger();

    public BatchExcelListener(int batchSize, Consumer<List<T>> processor) {
        this.batchSize = Math.max(500, batchSize);
        this.processor = processor;
    }

    @Override
    public void invoke(T data, AnalysisContext ctx) {
        if (isValid(data)) batch.add(data);
        else failed.incrementAndGet();
        if (batch.size() >= batchSize) processBatch();
        total.incrementAndGet();
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext ctx) {
        if (!batch.isEmpty()) processBatch();
    }

    private void processBatch() {
        try {
            processor.accept(new ArrayList<>(batch)); // 批处理(如批量入库)
            batch.clear();
        } catch (Exception e) {
            failed.addAndGet(batch.size());
            // 可加入重试:最多3次
        }
    }

    private boolean isValid(T d) { return d != null; } // 简化示例
}
  • 服务与并发读取多个 Sheet
@Service
public class ExcelImportService {
    @Autowired private YourDataService dataService;
    private final ExecutorService executor = Executors.newFixedThreadPool(8); // 按CPU/IO调整

    public void importMultiSheet(InputStream in) {
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < 20; i++) { // 假设20个Sheet
            int sheetNo = i;
            Future<?> f = executor.submit(() -> {
                EasyExcel.read(in, RowDto.class,
                        new BatchExcelListener<>(2000, batch -> dataService.batchInsert(batch)))
                        .sheet(sheetNo).doRead();
            });
            futures.add(f);
        }
        // 等待全部完成
        for (Future<?> f : futures) { try { f.get(); } catch (Exception ignore) {} }
        executor.shutdown();
    }
}
  • 异步任务编排(Spring Boot)
@RestController
public class ImportController {
    @Autowired private ExcelImportService importService;
    @Autowired private TaskService taskService;

    @PostMapping("/import")
    public CommonResult start(@RequestParam("file") MultipartFile file) {
        String taskId = taskService.createTask();
        CompletableFuture.runAsync(() -> {
            try (InputStream in = file.getInputStream()) {
                importService.importMultiSheet(in);
                taskService.complete(taskId, "SUCCESS");
            } catch (Exception e) {
                taskService.fail(taskId, e.getMessage());
            }
        }, taskExecutor());
        return CommonResult.ok(taskId);
    }

    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
        ex.setCorePoolSize(4); ex.setMaxPoolSize(8);
        ex.setQueueCapacity(50); ex.setThreadNamePrefix("import-");
        ex.initialize();
        return ex;
    }
}
  • 数据库批量插入(MyBatis 示例)
<insert id="batchInsert" parameterType="list">
  INSERT INTO your_table(col1, col2) VALUES
  <foreach collection="list" item="e" separator=",">
    (#{e.col1}, #{e.col2})
  </foreach>
</insert>

:通过流式读取 + 分批批量写入 + 异步并发,可稳定支撑十万至百万级数据导入;在合理参数与数据库优化配合下,导入耗时与内存占用均可控。

更多推荐