核心思路:流式读取 Excel(避免 OOM)+ 分批次批量插入(提升写入速度),推荐使用 EasyExcel 处理大文件,搭配 MyBatis/JDBC 做批量入库
一、核心依赖引入
Maven 依赖
<!-- EasyExcel 流式读取大文件 Excel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- MyBatis / Spring JDBC(二选一,这里用 MyBatis 示例) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
二、步骤 1:定义数据实体类
对应 Excel 列和数据库表结构,用 EasyExcel 注解映射列名:
import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; @Data public class StockData { // Excel 列名:商品ID @ExcelProperty(index = 0) private Long productId; // Excel 列名:库存数量 @ExcelProperty(index = 1) private Integer stockNum; // Excel 列名:版本号 @ExcelProperty(index = 2) private Integer version; }
三、步骤 2:流式读取监听器(核心,避免 OOM)
EasyExcel 监听器负责逐行读取 Excel,攒够一批数据后批量入库,不会把全量数据加载到内存:
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.read.listener.ReadListener; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @Slf4j public class StockDataListener implements ReadListener<StockData> { // 每批插入的批次大小(根据内存调整,推荐 1000~5000) private static final int BATCH_SIZE = 1000; // 临时存储当前批次数据 private List<StockData> batchData = new ArrayList<>(BATCH_SIZE); // 注入 Mapper(Spring 环境下) private final StockDataMapper stockDataMapper; public StockDataListener(StockDataMapper stockDataMapper) { this.stockDataMapper = stockDataMapper; } // 每读取一行数据触发 @Override public void invoke(StockData data, AnalysisContext context) { // 数据校验(可选:非空、格式校验) if (data.getProductId() == null || data.getStockNum() == null) { log.warn("跳过无效数据:{}", data); return; } batchData.add(data); // 达到批次大小,执行批量插入 if (batchData.size() >= BATCH_SIZE) { batchInsert(); batchData.clear(); } } // 所有数据读取完成后触发 @Override public void doAfterAllAnalysed(AnalysisContext context) { // 插入最后不足一批的数据 if (!batchData.isEmpty()) { batchInsert(); batchData.clear(); } log.info("Excel 数据导入完成!"); } // 批量插入数据库(事务控制) @Transactional(propagation = Propagation.REQUIRES_NEW) // 每批独立事务 public void batchInsert() { try { stockDataMapper.batchInsert(batchData); log.info("成功插入 {} 条数据", batchData.size()); } catch (Exception e) { log.error("批量插入失败,回滚当前批次", e); throw e; // 抛出异常触发事务回滚 } } }
四、步骤 3:MyBatis 批量插入 Mapper
Mapper 接口
import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface StockDataMapper { // 批量插入 int batchInsert(List<StockData> list); }
Mapper XML
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.StockDataMapper"> <insert id="batchInsert" parameterType="java.util.List"> INSERT INTO t_stock (product_id, stock_num, version) VALUES <foreach collection="list" item="item" separator=","> (#{item.productId}, #{item.stockNum}, #{item.version}) </foreach> </insert> </mapper>
五、步骤 4:业务层调用入口
import com.alibaba.excel.EasyExcel; import org.springframework.stereotype.Service; import java.io.File; @Service public class ExcelImportService { private final StockDataMapper stockDataMapper; public ExcelImportService(StockDataMapper stockDataMapper) { this.stockDataMapper = stockDataMapper; } public void importExcel(String filePath) { // 流式读取 Excel 文件 EasyExcel.read(new File(filePath), StockData.class, new StockDataListener(stockDataMapper)) .sheet() // 读取第一个 sheet .doRead(); // 开始读取 } }
六、关键性能优化(百万级必做)
1. 流式读取避免 OOM
- EasyExcel 底层是逐行解析,不会把整个 Excel 加载到内存,完美解决百万行文件 OOM 问题。
- 对比 POI:POI 会把整个 Excel 解析为对象树,百万行直接内存溢出。
2. 批量插入减少 IO 开销
- 禁止单条
INSERT循环执行,网络 IO 开销极大。 - 推荐批次大小:1000~5000 条 / 批,平衡内存和写入速度。
3. 数据库层面优化
- 临时删除索引:导入前删除非主键索引,导入完成后重建索引(插入时维护索引会拖慢 10~100 倍速度)。
- 临时调整 MySQL 参数:
SET GLOBAL innodb_flush_log_at_trx_commit = 2; -- 降低刷盘频率 SET GLOBAL sync_binlog = 0; -- 关闭 binlog 同步
- 导入完成后改回默认值(
1和1)保证数据安全。 - 关闭自动提交:JDBC 层面
connection.setAutoCommit(false),批量提交事务。
4. 并发优化(可选)
- 超大文件可拆分为多个小文件,用线程池并发读取 + 批量插入,控制线程数(不超过 CPU 核心数 * 2),避免压垮数据库。
- 注意:并发写入时需避免主键冲突,提前规划主键生成策略(如自增 ID、分布式 ID)。
七、注意事项
- 编码问题:Excel 保存为 UTF-8 编码,数据库连接串指定
characterEncoding=utf8mb4,避免中文乱码。 - 数据校验:在监听器
invoke方法中提前做格式校验,过滤脏数据,避免入库失败。 - 异常重试:批量插入失败时,记录失败批次,可单独重试该批次,避免重新导入全量数据。
- 事务选择:百万级数据不建议用一个大事务,会锁表导致其他业务阻塞,推荐每批一个小事务。
八、测试调用
@SpringBootTest public class ExcelImportTest { @Autowired private ExcelImportService excelImportService; @Test public void testImport() { // 百万级 Excel 文件路径 String filePath = "/data/million_stock_data.xlsx"; excelImportService.importExcel(filePath); } }
最终效果
百万级数据导入时间可控制在 分钟级(取决于数据库性能和批次大小),相比单条插入速度提升 100 倍以上,且不会出现 OOM
EasyExcel 会流式读 Excel → 读到一行 → 丢给监听器
invoke() → 攒够 1000 条 → 批量插入数据库 → 读完后收尾插入剩余数据Excel 全部读完
EasyExcel 自动调用:
@Override public void doAfterAllAnalysed(AnalysisContext context) { // 插入最后不足1000条的 if (!batchData.isEmpty()) { batchInsert(); } }
用最通俗的比喻
你要把 100 万块砖搬到仓库:
- EasyExcel 是搬砖工人,一块一块从车上拿(流式读取,不堆在手里)
- invoke 是把砖放进小推车
- 小推车装满 1000 块 → batchInsert 一次性推去仓库
- 推完回来继续装
- 最后车上剩一点,也推过去
全程不会把 100 万块砖堆在你怀里 → 不会 OOM
关键点总结
- doRead () 开始真正解析
- 每读一行 → 回调
invoke() - 攒够一批 → 批量插入
- 读完 →
doAfterAllAnalysed()插入剩余数据 - 全程流式读取,内存只存一个批次,不会爆内存
浙公网安备 33010602011771号