直接用SetSkip+SetLimit导出几万条数据会卡死,因MongoDB的SetSkip需扫描并丢弃所有被跳过文档,如page=1000、page_size=100时实际扫描10万条,导致CPU和I/O飙升、响应超时;Gin路由虽不阻塞,但collection.Find()卡住goroutine,影响并发处理。

为什么直接用 SetSkip + SetLimit 导出几万条数据会卡死
因为 MongoDB 的 SetSkip 是跳过前 N 条再取,底层仍要扫描并丢弃所有被跳过的文档。当 page=1000、page_size=100 时,实际扫描 10 万条,CPU 和磁盘 I/O 暴涨,响应延迟飙升甚至超时。Gin 路由本身不阻塞,但阻塞在 collection.Find() 这一步,整个 goroutine 卡住,无法并发处理其他请求。
用游标分页替代 offset 分页导出
游标分页靠排序字段(如 _id 或 created_at)做边界条件,避免跳过大量数据。关键点不是“第几页”,而是“从哪条之后继续”。导出场景下,你不需要支持随机跳页,只要保证顺序遍历、不漏不重即可。
- 必须确保排序字段有索引,否则
created_at > ?查询也会慢 - 首次请求不带游标,查最小值作为起点:
bson.M{"created_at": bson.M{"$exists": true}}+options.Find().SetSort(bson.D{{"created_at", 1}}).SetLimit(1) - 后续请求传
cursor=2025-06-01T08:30:00Z,查询条件为bson.M{"created_at": bson.M{"$gt": cursorValue}} - 每次最多取 500 条(不是 100),减少网络往返次数,也降低内存峰值
Gin 路由里怎么安全地流式返回 Excel 文件
导出接口不能一次性把全部数据加载进内存再写文件,否则 10 万条记录可能吃光几百 MB 内存。要用 c.Writer 直接写响应体,并手动设置 Content-Disposition 头。
- 调用
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - 设置
c.Header("Content-Disposition", "attachment; filename=export_"+time.Now().Format("20060102_150405")+".xlsx") - 用
excelize库创建*excelize.File后,调用f.Write(c.Writer)—— 注意不是f.SaveAs(),后者写本地磁盘 - 每写完一个 sheet 或一批行后,显式调用
c.Writer.Flush(),防止缓冲区堆积
如何避免导出中途客户端断连导致 Goroutine 泄漏
Gin 默认不检测连接是否已断开,如果用户关掉浏览器或网络中断,后端还在拼命查数据库、生成 Excel,goroutine 就卡在 IO 上一直不退出。
立即学习“go语言免费学习笔记(深入)”;
- 在 handler 开头加
ctx := c.Request.Context(),所有 DB 查询和文件写入都要传这个 context - mongo-go-driver 的
Find()支持 context,一旦客户端断连,ctx.Done()触发,查询自动 cancel - 用
select { case 在循环中定期检查上下文状态 - 不要用
time.Sleep做节流,改用ctx.Done()配合time.After实现带取消的等待


















