|

Aimee

Write the Code. Change the World.

大数据量导出怎么做:流式写入与异步任务

· 分享镜

大数据量导出怎么做:流式写入与异步任务

大数据量导出的经典翻车路径:本地测几十条没问题,一到线上的真实数据量,内存飙升直至 OOM。

问题几乎都出在同一种写法上:

// 问题代码
List<OrderRecord> all = orderDao.findAll(query);   // 一次性把 10 万行全捞到内存
Workbook wb = buildExcel(all);                     // 再占一份内存
response.getOutputStream().write(wb.toByteArray()); // 再压一份到字节数组

三份数据同时在内存里。10 万行,每行几十个字段,几百 MB 起步。


分页查询 + 流式写入

不能一次全捞,改成分批读、边读边写:

// 使用 EasyExcel 流式写入(写完即刷,不在内存积累)
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream(), OrderRecord.class).build()) {
    WriteSheet sheet = EasyExcel.writerSheet("订单").build();
    
    int page = 0, size = 1000;
    List<OrderRecord> batch;
    do {
        batch = orderDao.findPage(query, page++, size);
        writer.write(batch, sheet);
        batch.clear(); // 尽快 GC
    } while (batch.size() == size);
}

每次只有 1000 行在内存,EasyExcel 写完这批就刷到输出流,内存占用基本恒定。


同步导出的问题

即使用了流式写入,同步接口还有另一个坑:

  • 10 万行查询 + 写入可能要 30 秒
  • HTTP 超时(Nginx 默认 60s)
  • 用户等待体验差

数据量超过 1 万行,换成异步任务

用户点导出 → 后端创建导出任务(task_id)→ 立即返回
                          ↓(异步)
             Worker 查询 + 生成文件 + 上传 OSS
                          ↓
             推送通知 or 轮询接口 → 用户拿到下载链接
// 创建任务
public String submitExport(ExportQuery query, long userId) {
    ExportTask task = new ExportTask(userId, query, TaskStatus.PENDING);
    taskDao.insert(task);
    mqProducer.send(new ExportTaskMessage(task.getId()));
    return task.getId();
}

// 查询任务状态
public ExportTaskVO getStatus(String taskId) {
    ExportTask task = taskDao.getById(taskId);
    return new ExportTaskVO(task.getStatus(), task.getDownloadUrl());
}

文件放哪

不要让文件从服务器直接走 HTTP 流输出给用户——并发导出时,服务器带宽会被占满。

标准做法:生成的文件上传到 OSS(阿里云/腾讯云/MinIO),生成带过期时间的下载链接(7 天内有效)返回给用户。用户直接从 OSS 拉文件,不走你的服务器。


导出任务的幂等

同一个导出请求,用户重复点击会创建多个任务。在任务创建时加幂等键(如 userId + 查询参数 hash),同一参数在 N 分钟内只创建一个任务,返回同一个 taskId:

String idempotentKey = DigestUtils.md5Hex(userId + JSON.toJSONString(query));
ExportTask existing = taskDao.getByIdempotentKey(idempotentKey);
if (existing != null && existing.isRecentlyCreated()) {
    return existing.getId();
}

避免用户着急多次点击,堆了几十个相同的导出任务。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部