大数据量异步导出方案

导出 500 万条数据生成 Excel 时,内存、超时、并发等问题都需要系统化的方案设计。核心思路:异步化 + 分批处理 + 流式写入

整体架构

请求 → 控制器 → 任务管理器 → 异步处理线程池 → 分批查询数据库 → 流式写入Excel → 上传OSS
                                                                          ↓
前端轮询 ← 状态查询 ← 状态表(MySQL/Redis) ← 任务状态更新 → 完成后通知用户

分层职责

层级职责技术选型
请求层接收导出请求,生成任务 ID,立即返回REST API
任务管理层任务状态管理(排队/执行/完成/失败/取消)数据库表 + Redis
数据处理层分批查询,流式读取,避免 OOMMyBatis Cursor / JDBC 流式
文件生成层流式写入 Excel,控制内存SXSSFWorkbook(POI)
结果持久化上传对象存储,提供预签名下载链接OSS / MinIO / S3

分批读取策略

1. MyBatis Cursor(游标查询)

@Mapper
public interface UserMapper {
    // MyBatis Cursor 游标查询 — 不一次性加载到内存
    @Select("SELECT * FROM user WHERE export_batch_id = #{batchId}")
    Cursor<User> streamExportData(@Param("batchId") Long batchId);
}
// 使用方式 — 逐个消费
try (Cursor<User> cursor = userMapper.streamExportData(batchId)) {
    cursor.forEach(user -> {
        // 每读取一条,写入一行
        rowData = buildRow(user);
        sheet.writeRow(rowData);
    });
}

2. 基于主键 ID 分段查询(推荐)

// 每次查询 5000 条
Long maxId = 0L;
int batchSize = 5000;
boolean hasMore = true;
 
while (hasMore) {
    List<User> batch = userMapper.selectByIdRange(maxId, batchSize);
    if (batch.isEmpty()) {
        hasMore = false;
    } else {
        writeToExcel(batch);
        maxId = batch.get(batch.size() - 1).getId();
    }
}
-- 利用主键索引,性能最优
SELECT * FROM user WHERE id > #{maxId} ORDER BY id LIMIT #{batchSize}

3. MySQL Streaming ResultSet

// JDBC 原生流式读取
statement = connection.prepareStatement("SELECT * FROM user");
statement.setFetchSize(Integer.MIN_VALUE); // 关键:触发流式读取
resultSet = statement.executeQuery();
 
while (resultSet.next()) {
    // 逐行处理,不加载到内存
}

注意setFetchSize(Integer.MIN_VALUE) 会告诉 MySQL 驱动逐行流式读取,避免 ResultSet 把所有数据加载到 JVM 内存。

文件生成方案

SXSSFWorkbook(流式写入)

<!-- Apache POI 依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>
// SXSSFWorkbook — 流式写入,控制内存
SXSSFWorkbook workbook = new SXSSFWorkbook();
workbook.setCompressTempFiles(true); // 压缩临时文件
SXSSFSheet sheet = workbook.createSheet("导出数据");
// 指定内存中保留的行数,超出的写临时文件
sheet.setRandomAccessWindowSize(100);
 
// 分批写入
int rowNum = 0;
while (hasMore) {
    List<User> batch = fetchBatch();
    for (User user : batch) {
        Row row = sheet.createRow(rowNum++);
        // 写入每一行...
    }
}
 
// 写入输出流
try (OutputStream out = new FileOutputStream(tempFile)) {
    workbook.write(out);
}
workbook.dispose(); // 释放临时文件

进度通知与结果

前端轮询

// 任务状态查询 API
@GetMapping("/export/status/{taskId}")
public Result<ExportStatus> getExportStatus(@PathVariable String taskId) {
    ExportStatus status = exportTaskService.getStatus(taskId);
    return Result.success(status);
}

WebSocket 推送(可选)

// 进度完成时推送通知
webSocketHandler.sendMessage(userId, ExportEvent.completed(taskId, downloadUrl));

数据量监控与限流

  • 设置最大并发导出数(如同时最多 N 个导出任务)
  • 控制每批次大小(通常 5000-10000 条/批)
  • 单次导出上限(如最大 500 万条,超过提示缩小范围)
  • 总内存监控:SXSSFWorkbook 写满 100 行刷入磁盘

参考链接