核心思路:流式读取 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 同步
  • 导入完成后改回默认值(11)保证数据安全。
  • 关闭自动提交:JDBC 层面 connection.setAutoCommit(false),批量提交事务。

4. 并发优化(可选)

  • 超大文件可拆分为多个小文件,用线程池并发读取 + 批量插入,控制线程数(不超过 CPU 核心数 * 2),避免压垮数据库。
  • 注意:并发写入时需避免主键冲突,提前规划主键生成策略(如自增 ID、分布式 ID)。

 

七、注意事项

  1. 编码问题:Excel 保存为 UTF-8 编码,数据库连接串指定 characterEncoding=utf8mb4,避免中文乱码。
  2. 数据校验:在监听器 invoke 方法中提前做格式校验,过滤脏数据,避免入库失败。
  3. 异常重试:批量插入失败时,记录失败批次,可单独重试该批次,避免重新导入全量数据。
  4. 事务选择:百万级数据不建议用一个大事务,会锁表导致其他业务阻塞,推荐每批一个小事务。
八、测试调用
@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 万块砖搬到仓库:
  1. EasyExcel 是搬砖工人,一块一块从车上拿(流式读取,不堆在手里)
  2. invoke 是把砖放进小推车
  3. 小推车装满 1000 块 → batchInsert 一次性推去仓库
  4. 推完回来继续装
  5. 最后车上剩一点,也推过去
全程不会把 100 万块砖堆在你怀里 → 不会 OOM

关键点总结

  1. doRead () 开始真正解析
  2. 每读一行 → 回调 invoke()
  3. 攒够一批 → 批量插入
  4. 读完 → doAfterAllAnalysed() 插入剩余数据
  5. 全程流式读取,内存只存一个批次,不会爆内存
 
posted on 2026-07-29 23:20  从精通到陌生  阅读(37)  评论(0)    收藏  举报