·更新于 2026年10月25日· 22 次浏览 Node.jsMongoDBRedis工程实践BUG解决

周报导出拖垮 MongoDB:我用流式游标和 Redis 队列改成异步下载

一次周报导出接口慢查询和内存飙升,我把同步大查询改成异步任务:Redis 队列管状态,MongoDB 流式游标分批读,最后生成临时下载链接。

周报导出拖垮 MongoDB:我用流式游标和 Redis 队列改成异步下载 封面
ZHOUYI

作者

ZHOUYI

上周运营导周报,点一下导出,接口转圈两分钟,MongoDB 慢查询告警,Node 进程内存也飙上去。原因很直接:一次性 find 几十万条文档,再在内存里拼 CSV。同步导出这种活,不能挂在 HTTP 请求里硬扛。

把导出改成异步任务

我改成点击导出先创建任务:写入任务表或 Redis,状态设为 queued,立刻返回 taskId。前端轮询任务状态,不再等待长连接。Worker 从 Redis 队列取任务,按时间范围和用户维度分批查询。这样接口只负责下单,查询压力被挪到后台,还能限制并发。

流式游标分批读

javascript
const cursor = db.collection('weekly_events')
  .find({ createdAt: { $gte: start, $lte: end } })
  .sort({ _id: 1 })
  .batchSize(1000);

let buffer = [];
for await (const doc of cursor) {
  buffer.push(doc);
  if (buffer.length >= 1000) {
    await appendCsv(buffer);
    buffer.length = 0;
  }
}
if (buffer.length) await appendCsv(buffer);

关键是不要 toArray。用 cursor 逐条读,攒满一批就追加写文件,写完清空数组。每批之间还能让出事件循环,避免 CPU 一直占满。查询条件最好走索引,并按 _id 或时间排序,避免深分页。

状态、重试和下载链接要闭环

  1. 任务状态至少包含 queued、running、success、failed,前端按状态展示进度。
  2. 失败重试要带次数上限和退避时间,重试时重新分批,不能把半成品文件当成成功。
  3. 成功后生成临时下载链接或对象存储签名 URL,过期后重新生成,别把本地磁盘路径直接返回。

“踩坑点:任务状态别只放内存,Node 重启就丢了;下载文件最好带任务 ID 命名,方便排障和清理。若用 Redis 做锁或队列,也要考虑过期和异常释放。”

改完后,导出接口基本秒回,MongoDB 的瞬时压力变成平稳的小批量读取。对后台报表这类需求,异步任务、流式读取、可追踪状态,比继续优化单次大查询更靠谱。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?