大数据量异步导出方案
导出 500 万条数据生成 Excel 时,内存、超时、并发等问题都需要系统化的方案设计。核心思路:异步化 + 分批处理 + 流式写入。
整体架构
请求 → 控制器 → 任务管理器 → 异步处理线程池 → 分批查询数据库 → 流式写入Excel → 上传OSS
↓
前端轮询 ← 状态查询 ← 状态表(MySQL/Redis) ← 任务状态更新 → 完成后通知用户
分层职责
| 层级 | 职责 | 技术选型 |
|---|---|---|
| 请求层 | 接收导出请求,生成任务 ID,立即返回 | REST API |
| 任务管理层 | 任务状态管理(排队/执行/完成/失败/取消) | 数据库表 + Redis |
| 数据处理层 | 分批查询,流式读取,避免 OOM | MyBatis 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 行刷入磁盘
参考链接
- 快手电商-一面-19题总结 — Q15 大数据量导出
- 接口幂等方案设计 — 防止重复提交
- 线程池拒绝策略 — 异步任务拒绝策略
- JVM-OOM排查指南 — 防止 OOM 的内存管理