toarray() 会 oom 或卡死,因其将全部结果集加载到应用内存;应改用 mongocursor + next() 分批处理,并配合范围查询实现高效分页与游标容错。

为什么 toArray() 会 OOM 或卡死
游标本身只是数据库端的一个指针,不加载数据;但调用 toArray() 会强制把整个结果集拉到应用内存里。几十万条文档一并加载,Java 可能抛 OutOfMemoryError,Node.js 会卡住甚至进程退出,Python 的 list(cursor) 同理。
常见错误现象包括:接口超时、服务内存持续上涨、日志里出现 cursor id not valid(因游标在服务端被自动销毁后还试图重用)。
- 永远不要对未知数量的结果调用
toArray()或list() - 如果必须全量处理,改用
forEach()或手动next()迭代 - 注意驱动默认的
batchSize(如 Java 驱动默认 101 条/批),它影响网络往返次数,但不改变内存压力本质
用 MongoCursor + next() 安全遍历百万级数据
这是最直接可控的方式,适用于后台批处理、ETL 或导出场景。核心是“取一批、处理一批、再取下一批”,避免累积。
以 Java 为例(Spring Data MongoDB):
MongoCursor<document> cursor = collection.find(query)
.sort(Sorts.descending("created_at"))
.batchSize(500) // 每次从服务端拉 500 条,非总条数
.iterator();
while (cursor.hasNext()) {
Document doc = cursor.next();
process(doc); // 单条处理,不缓存
}</document>
关键点:
-
batchSize是建议值,MongoDB 可能返回更少(如最后一批不足) - 不要在循环中调用
cursor.count()—— 它会重新执行查询,且不复用游标状态 - 若处理耗时较长(>10 分钟),需显式设置游标超时:
.noCursorTimeout(true),否则服务端可能回收游标
用排序字段 + 范围查询替代 skip() 实现真分页
当你要支持前端“下一页”按钮,且数据量超过 10 万,skip() 已经不可靠:第 10000 页要跳过 99990 条,MongoDB 得扫描并丢弃它们,CPU 和 I/O 成倍增长。
正确做法是基于上一页最后一条记录的排序字段值做范围查询:
// 第一页(按 _id 升序)
db.collection.find().sort({_id: 1}).limit(10)
// 第二页:取上一页最后一条的 _id,比如 ObjectId("60a1b2c3d4e5f67890123456")
db.collection.find({_id: {$gt: ObjectId("60a1b2c3d4e5f67890123456")}})
.sort({_id: 1})
.limit(10)
适用前提和限制:
- 必须有**高选择性、单调递增(或严格有序)的字段**,如
_id、created_at、自增数字 ID;时间戳字段要注意精度和重复问题 - 需要为该字段建索引,否则范围查询也慢
- 不支持“跳转到第 N 页”,只适合线性翻页;用户想搜第 500 页,得从前一页一页推进(实际业务中极少发生)
游标失效与连接中断的应对策略
游标不是永久句柄。MongoDB 默认 10 分钟无活动就回收,网络抖动、GC 停顿、下游服务响应慢都可能导致 CursorNotFoundException。
实操建议:
- 加 try-catch 包裹
next(),捕获CursorNotFoundException后,根据当前已处理的最大排序值(如最后成功处理的created_at)重建查询条件,继续拉取 - 避免长事务式处理:单次循环体逻辑尽量轻量,必要时用
Thread.sleep(1)控制节奏,防止压垮 DB - 生产环境务必开启
maxTimeMS,例如.maxTimeMS(30000),防止单次查询无限 hang 住
真正棘手的不是怎么写循环,而是怎么让循环在异常、重启、扩容后还能接着上次的位置继续——这要求你把“断点”(如最后处理的 _id 或时间戳)落盘或写入 Redis,而不是只存在内存变量里。











