乐于分享
好东西不私藏

【Java】千万行 Excel 导入数据库?Sheet 上限、EasyExcel、批量与对账

【Java】千万行 Excel 导入数据库?Sheet 上限、EasyExcel、批量与对账

千万行 Excel 导入踩过 3 个坑,OOM、慢、丢数全都中过

业务扔过来一句「这个 Excel 要导进库,一千万行」。大多数人直接开始撸代码,结果第一步就错了:单 Sheet 根本装不下。这篇把 OOM、慢、丢数三个根本原因和对应方案说清楚,不绕弯子。

① Sheet 行数天花板——写代码前先算清楚

千万级数据写代码前,先确认文件结构能不能装下。单 Sheet 有硬性上限:

格式
单 Sheet 最大行数
够装千万级?
.xls(Excel 2003)
65,536 行
.xlsx(Excel 2007+)
1,048,576 行(约 104 万)
CSV
无硬性限制
✅ 推荐分段传

千万行必须拆多 Sheet拆多文件,或改用 CSV 分段传输。行数规格以 Microsoft 官方文档 为准。

② 解决 OOM:流式读取,不整表加载

用 POI 一次性把文件读成 List 是 OOM 的直接原因。换 EasyExcel(github.com/alibaba/easyexcel)做流式回调,每攒够一批就落库,堆内存始终可控:

// 每读满 5000 行触发 flush,不把整文件加载进内存EasyExcel.read(filePath, Row.class, new AnalysisEventListener<Row>() {     private final List<Row> buf = new ArrayList<>();     @Override public void invoke(Row row, AnalysisContext ctx) {         buf.add(row);         if (buf.size() >= 5000) { flush(buf); buf.clear(); }     }     @Override public void doAfterAllAnalysed(AnalysisContext ctx) {         if (!buf.isEmpty()) flush(buf);     }     void flush(List<Row> b) { batchSave(b); } }).sheet().doRead();

百万行内放单 Sheet 问题不大;超千万需拆文件或 CSV,EasyExcel 3.1.1+ 已支持 CSV 读取(以 Release Notes 为准)。

③ 解决慢:批量分片 + 多线程

循环里一条条 INSERT 是性能杀手,每次都触发一次远程 IO。改批量插入,单批用 Guava Lists.partition 切成 1000 条一组再提交(批次大小需压测,过大易超时):

// 先做完业务逻辑,再统一分片写库List<Entity> processed = doBusinessLogic(batch); Lists.partition(processed, 1000)      .forEach(sub -> mapper.batchInsert(sub));

业务逻辑复杂、单批处理耗时长时,可用 CompletableFuture 并发处理,但务必显式传入自定义线程池(不传默认 ForkJoinPool,高并发下行为不可控),且线程数不宜过多——上下文切换会反噬 CPU:

// executor 为业务自定义线程池,线程数建议压测后配置化List<Entity> result = new CopyOnWriteArrayList<>(); CompletableFuture.allOf(     batch.stream()          .map(d -> CompletableFuture              .supplyAsync(() -> process(d), executor)              .whenComplete((r, ex) -> { if (r != null) result.add(r); }))          .toArray(CompletableFuture[]::new) ).join(); mapper.batchInsert(result);

④ 解决丢数:对账 + 唯一索引 + 失败重试

数据量越大,异常越难发现。以下三条缺一不可:

措施
做法
目的
条数对账
导入结束后对比「Excel 源行数」与「库中新增数」
发现静默丢数
批次日志
失败时记录批次号 + 首尾主键,自动重试 2 次
快速定位哪批挂了
唯一索引 + INSERT IGNORE
业务唯一键加索引,并发写入用 INSERT IGNORE
防重复导入,不中断整批

⚠️ 高频踩坑:单批超过 5000 条写库容易触发 DB 超时或锁等待;线程数配置太高会导致 CPU 使用率飙升。两个数字都应通过压测确定,而不是凭经验拍。

💡 原生 JDBC addBatch 性能更高: ORM(MyBatis/Hibernate)底层有反射开销。若批量写入成为瓶颈,可评估在关键路径换用 JDBC PreparedStatement.addBatch() / executeBatch(),代价是多写样板代码——要用压测数据说话,而非提前优化。

总结

  • 千万级数据先算 Sheet 上限,必须拆文件或改 CSV,再谈读写方案
  • 流式读(EasyExcel)+ 分片批量写(Guava partition 1000)+ 自定义线程池并发处理,三板斧缺一不可
  • 导入结束必须做条数对账;唯一索引 + INSERT IGNORE 防重复;失败批次打日志记首尾主键

#Java#后端开发#Excel导入#EasyExcel#性能优化#全栈程序员