Java 大数据量批处理性能优化指南

一、批处理核心原则

单条处理:20万次网络往返 + 20万次SQL解析 = 极慢
批量处理:200次网络往返 + 200次SQL解析 = 快1000倍

批次大小选择平衡点

  • 太小(1~100):网络往返次数多,总耗时长
  • 太大(10000+):单次内存占用高,SQL 过长,数据库锁持有时间长
  • 最佳区间:1000~5000

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、导入优化

2.1 批量 INSERT(核心)

// 慢:逐条 INSERT(20万次网络IO)
for (Entity e : list) {
    mapper.insert(e);  // 每条一次网络往返
}

// 快:批量 INSERT(100次网络IO)
for (int i = 0; i < list.size(); i += 2000) {
    int end = Math.min(i + 2000, list.size());
    mapper.batchInsert(list.subList(i, end));
}

XML:

<insert id="batchInsert">
  INSERT INTO my_table (col1, col2, col3) VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.col1}, #{item.col2}, #{item.col3})
  </foreach>
</insert>

2.2 分页处理模式

int pageSize = 2000;
int page = 1;
while (true) {
    List<Detail> batch = mapper.selectPage(taskId, page, pageSize);
    if (batch.isEmpty()) break;

    // 校验或入库
    processBatch(batch);

    // 回写状态
    mapper.batchUpdateStatus(batch);
    page++;
}

2.3 完整导入示例

public void importData(List<RawData> allData) {
    int batchSize = 2000;
    int total = allData.size();

    for (int i = 0; i < total; i += batchSize) {
        int end = Math.min(i + batchSize, total);
        List<RawData> batch = allData.subList(i, end);

        // 1. 校验
        List<Entity> validList = new ArrayList<>();
        for (RawData raw : batch) {
            if (validate(raw)) {
                validList.add(convert(raw));
            }
        }

        // 2. 批量入库
        if (!validList.isEmpty()) {
            mapper.batchInsert(validList);
        }
    }
}

三、导出优化

3.1 SXSSFWorkbook 流式写入

SXSSFWorkbook workbook = new SXSSFWorkbook(200); // 窗口200行
Sheet sheet = workbook.createSheet("数据");

int pageSize = 5000;
int page = 1;
int rowIndex = 1;

while (true) {
    List<Entity> batch = mapper.selectPage(page, pageSize);
    if (batch.isEmpty()) break;

    for (Entity e : batch) {
        Row row = sheet.createRow(rowIndex++);
        row.createCell(0).setCellValue(e.getName());
        row.createCell(1).setCellValue(e.getAmount());
    }
    page++;
}

workbook.write(outputStream);
workbook.dispose(); // 清理临时文件

3.2 EasyExcel 流式写入

ExcelWriter writer = EasyExcelFactory.write(outputStream, ExportRow.class).build();
WriteSheet sheet = EasyExcelFactory.writerSheet("数据").build();

int page = 0;
while (true) {
    List<ExportRow> batch = queryData(param, page, 5000);
    if (batch.isEmpty()) break;
    writer.write(batch, sheet);
    page++;
}
writer.finish();

四、方案对比

导入方案

方案 20万条耗时 内存 复杂度
逐条INSERT 15~30分钟
批量INSERT(2000/批) 5~10秒 中(25MB)
MQ异步+批量INSERT 5~10秒(用户无感)
线程池异步+批量INSERT 3~8秒(用户无感)

导出方案

方案 20万条耗时 内存 文件大小
XSSFWorkbook全内存 15~25秒 400MB+ 8~12MB
SXSSFWorkbook+分页查 4~8秒 20~30MB 8~12MB
EasyExcel流式 3~6秒 15~25MB 8~12MB
CSV 1~3秒 <5MB 15~20MB

五、完整示例

@Service
public class BatchProcessService {

  @Resource
  private DataMapper dataMapper;

  private static final int BATCH_SIZE = 2000;

  /** 批量导入. */
  public void batchImport(List<ImportRow> rawData) {
    List<DataEntity> buffer = new ArrayList<>(BATCH_SIZE);

    for (ImportRow raw : rawData) {
      if (!validate(raw)) continue;
      buffer.add(convert(raw));

      if (buffer.size() >= BATCH_SIZE) {
        dataMapper.batchInsert(buffer);
        buffer.clear();
      }
    }
    // 处理剩余
    if (!buffer.isEmpty()) {
      dataMapper.batchInsert(buffer);
    }
  }

  /** 流式导出. */
  public void streamExport(OutputStream out, QueryParam param) {
    SXSSFWorkbook wb = new SXSSFWorkbook(200);
    try {
      Sheet sheet = wb.createSheet("导出数据");
      writeHeader(sheet);

      int page = 1, rowIdx = 1;
      while (true) {
        List<DataEntity> batch = dataMapper.selectByPage(param, page, 5000);
        if (batch.isEmpty()) break;
        for (DataEntity e : batch) {
          writeRow(sheet, rowIdx++, e);
        }
        page++;
      }
      wb.write(out);
    } finally {
      wb.dispose();
    }
  }
}

六、关键配置

# 数据库连接池(批处理需要足够连接)
spring:
  datasource:
    hikari:
      maximum-pool-size: 20      # 并发批处理时需要足够连接
      connection-timeout: 30000

# MySQL批量操作需要的URL参数
      url: jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
      #                               ↑ 关键!开启批量重写,性能提升5~10倍

rewriteBatchedStatements=true 让 JDBC 驱动将多条 INSERT 合并为一条多值 INSERT,是批量写入性能的关键配置。

七、批量 UPDATE 优化

7.1 逐条 vs 批量

// 慢:逐条UPDATE
for (Detail d : list) {
    mapper.updateStatus(d.getId(), d.getStatus());  // 20万次
}

// 快:CASE WHEN 批量UPDATE
mapper.batchUpdateStatus(list);  // 按批次执行

XML 实现:

<update id="batchUpdateStatus">
  UPDATE exchange_task_detail
  SET status = CASE id
  <foreach collection="list" item="item">
    WHEN #{item.id} THEN #{item.status}
  </foreach>
  END,
  data = CASE id
  <foreach collection="list" item="item">
    WHEN #{item.id} THEN #{item.data}
  </foreach>
  END
  WHERE id IN
  <foreach collection="list" item="item" open="(" separator="," close=")">
    #{item.id}
  </foreach>
</update>

7.2 另一种方式:临时表 JOIN UPDATE

数据量极大时(50万+),可以先把要更新的数据写入临时表,再用 JOIN UPDATE:

-- 1. 创建临时表并插入数据
CREATE TEMPORARY TABLE tmp_update (id INT, status INT);
INSERT INTO tmp_update VALUES (1, 3), (2, 3), ...;

-- 2. JOIN UPDATE
UPDATE exchange_task_detail d
JOIN tmp_update t ON d.id = t.id
SET d.status = t.status;

八、并发批处理

8.1 线程池分片

public void parallelImport(List<Entity> allData) {
    int batchSize = 2000;
    ExecutorService executor = Executors.newFixedThreadPool(4);
    List<Future<?>> futures = new ArrayList<>();

    for (int i = 0; i < allData.size(); i += batchSize) {
        int start = i;
        int end = Math.min(i + batchSize, allData.size());
        List<Entity> batch = allData.subList(start, end);

        futures.add(executor.submit(() -> {
            mapper.batchInsert(batch);
        }));
    }

    // 等待全部完成
    for (Future<?> f : futures) {
        f.get();
    }
    executor.shutdown();
}

注意:并发写入时需确保没有唯一键冲突,且数据库连接池大小 >= 线程数。

8.2 性能对比

方式 20万条INSERT耗时 数据库连接占用
单线程 2000/批 5~10秒 1个
4线程 2000/批 2~4秒 4个
8线程 2000/批 1~2.5秒 8个

九、内存优化技巧

9.1 避免全量加载

// 错误:20万条全加载到内存
List<Entity> all = mapper.selectAll();  // ~200MB

// 正确:分页加载,用完即弃
int page = 1;
while (true) {
    List<Entity> batch = mapper.selectPage(page, 5000);  // ~5MB
    if (batch.isEmpty()) break;
    process(batch);
    page++;
    // batch 在下一次循环时可被GC
}

9.2 流式读取 Excel(导入时)

// EasyExcel 流式读取,不会把20万行全加载到内存
EasyExcelFactory.read(inputStream, RowData.class,
    new PageReadListener<RowData>(batch -> {
        // 每100行回调一次,处理后释放
        processBatch(batch);
    }, 100))
    .sheet()
    .doRead();

十、rewriteBatchedStatements 详解

这是 MySQL JDBC 驱动的关键参数:

jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
设置 实际执行的SQL 网络往返
false(默认) INSERT INTO t VALUES(1); INSERT INTO t VALUES(2); ... N次
true INSERT INTO t VALUES(1),(2),(3),... 1次

对批量 INSERT 性能提升 5~10倍。对 UPDATE 也有类似优化(合并为多条一次发送)。


十一、总结

导入优化公式:
  总耗时 = (数据量 / 批次大小) × 单批耗时
  单批耗时 = 网络往返(~2ms) + SQL执行(~5ms) + 业务处理(~1ms)
  
  20万条 / 2000批 × 8ms = 0.8秒(理想值)
  实际加上GC、连接等待等 ≈ 5~10秒

导出优化公式:
  总耗时 = 查询时间 + 写入时间
  查询 = (数据量 / 页大小) × 单页查询时间
  写入 = SXSSFWorkbook 流式写(不受内存限制)

批次大小选择:
  ┌─────────────┬──────────────────────────────┐
  │ 数据量      │ 推荐批次大小                   │
  ├─────────────┼──────────────────────────────┤
  │ < 1万       │ 1000                         │
  │ 1万~10万    │ 2000                         │
  │ 10万~50万   │ 2000~5000                    │
  │ > 50万      │ 5000 + 并发线程池             │
  └─────────────┴──────────────────────────────┘
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐