惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
A
About on SuperTechFans
Y
Y Combinator Blog
D
DataBreaches.Net
I
InfoQ
Recent Announcements
Recent Announcements
Last Week in AI
Last Week in AI
G
Google Developers Blog
博客园_首页
博客园 - 司徒正美
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
博客园 - 叶小钗
Engineering at Meta
Engineering at Meta
Apple Machine Learning Research
Apple Machine Learning Research
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
B
Blog
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
D
Docker
量子位

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
大数据量导出怎么做:流式写入与异步任务
Aimee · 2026-08-11 · via Aimee's Blog

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

大数据量导出的经典翻车路径:本地测几十条没问题,一到线上的真实数据量,内存飙升直至 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();
}

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