本文使用的编程语言是 Node.js,连接 MongoDB 的模块用的是mongoose。但是,本文介绍的方法适用于其他编程语言及其对应的 MongoDB 模块。

错误方法:find()

也许,在遍历 MongoDB 集合时,我们会这样写:

const Promise = require("bluebird");

function findAllMembers() {
return Member.find();
} async function test() {
const members = await findAllMembers();
let N = 0;
await Promise.mapSeries(members, member => {
N++;
console.log(`name of the ${N}th member: ${member.name}`);
});
console.log(`loop all ${N} members success`);
} test();

注意,我们使用的是 Bluebird 的mapSeries而非map,members 数组中的元素是一个一个处理的。这样就够了吗?

当 Member 集合中的 document 不多时,比如只有 1000 个时,那确实没有问题。但是当 Member 集合中有 1000 万个 document 时,会发生什么呢?如下:

<--- Last few GCs --->
rt of marking 1770 ms) (average mu = 0.168, current mu = 0.025) finalize [5887:0x43127d0] 33672 ms: Mark-sweep 1398.3 (1425.2) -> 1398.0 (1425.7) MB, 1772.0 / 0.0 ms (+ 0.1 ms in 12 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 1775 ms) (average mu = 0.088, current mu = 0.002) finalize [5887:0x43127d0] 35172 ms: Mark-sweep 1398.5 (1425.7) -> 1398.4 (1428.7) MB, 1496.7 / 0.0 ms (average mu = 0.049, current mu = 0.002) allocation failure scavenge might not succeed <--- JS stacktrace ---> FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0x8c02c0 node::Abort() [node]
2: 0x8c030c [node]
3: 0xad15de v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
4: 0xad1814 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
5: 0xebe752 [node]
6: 0xebe858 v8::internal::Heap::CheckIneffectiveMarkCompact(unsigned long, double) [node]
7: 0xeca982 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
8: 0xecb2b4 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
9: 0xecba8a v8::internal::Heap::FinalizeIncrementalMarkingIfComplete(v8::internal::GarbageCollectionReason) [node]
10: 0xecf1b7 v8::internal::IncrementalMarkingJob::Task::RunInternal() [node]
11: 0xbc1796 v8::internal::CancelableTask::Run() [node]
12: 0x935018 node::PerIsolatePlatformData::FlushForegroundTasksInternal() [node]
13: 0x9fccff [node]
14: 0xa0dbd8 [node]
15: 0x9fd63b uv_run [node]
16: 0x8ca6c5 node::Start(v8::Isolate*, node::IsolateData*, int, char const* const*, int, char const* const*) [node]
17: 0x8c945f node::Start(int, char**) [node]
18: 0x7f84b6263f45 __libc_start_main [/lib/x86_64-linux-gnu/libc.so.6]
19: 0x885c55 [node]
Aborted (core dumped)

可知,内存不足了。

打印find()返回的 members 数组可知,集合中所有元素都返回了,哪个数组放得下 1000 万个 Object?

正确方法:find().cursor()与 eachAsync()

将整个集合 find()全部返回,这种操作应该避免,正确的方法应该是这样的:

function findAllMembersCursor() {
return Member.find().cursor();
} async function test() {
const membersCursor = await findAllMembersCursor();
let N = 0;
await membersCursor.eachAsync(member => {
N++;
console.log(`name of the ${N}th member: ${member.name}`);
});
console.log(`loop all ${N} members success`);
} test();

使用cursor()方法返回 QueryCursor,然后再使用eachAsync()就可以遍历整个集合了,而且不用担心内存不够。

QueryCursor是什么呢?不妨看一下 mongoose 文档:

A QueryCursor is a concurrency primitive for processing query results one document at a time. A QueryCursor fulfills the Node.js streams3 API, in addition to several other mechanisms for loading documents from MongoDB one at a time.

总之,QueryCursor 可以每次从 MongoDB 中取一个 document,这样显然极大地减少了内存使用。

如何测试?

这篇博客介绍的内容很简单,但是也很容易被忽视。如果大家测试一下,印象会更加深刻一些。

测试代码很简单,大家可以查看Fundebug/loop-mongodb-big-collection

我的测试环境是这样的:

  • ubuntu 14.04
  • mongodb 3.2
  • nodejs 10.9.0

1. 使用 Docker 运行 MongoDB

sudo docker run --net=host -d --name mongodb daocloud.io/library/mongo:3.2

2. 使用mgodatagen生成测试数据

使用 mgodatagen,1000 万个 document 可以在 1 分多钟生成!

下载 mgodatagen:https://github.com/feliixx/mgodatagen/releases/download/0.7.3/mgodatagen_linux_x86_64.tar.gz

解压之后,复制到/usr/local/bin 目录即可:

sudo mv mgodatagen /usr/local/bin

mgodatagen 的配置文件mgodatagen-config.json如下:

[
{
"database": "test",
"collection": "members",
"count": 10000000,
"content": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"city": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"country": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"company": {
"type": "string",
"minLength": 2,
"maxLength": 8
},
"email": {
"type": "string",
"minLength": 2,
"maxLength": 8
}
}
}
]

执行mgodatagen -f mgodatagen-config.json命令,即可生成 10000 万测试数据。

mgodatagen -f mgodatagen-config.json
Connecting to mongodb://127.0.0.1:27017
MongoDB server version 3.2.13 collection members: done [====================================================================] 100% +------------+----------+-----------------+----------------+
| COLLECTION | COUNT | AVG OBJECT SIZE | INDEXES |
+------------+----------+-----------------+----------------+
| members | 10000000 | 108 | _id_ 95368 kB |
+------------+----------+-----------------+----------------+ run finished in 1m12.82s

查看 MongoDB,可知新生成的数据有 0.69GB,其实很小,但是使用 find()方法遍历会报错。

show dbs
local 0.000GB
test 0.690GB

3. 执行测试代码

两种不同遍历方法的代码分别位于test1.jstest2.js

参考

如何高效地遍历 MongoDB 超大集合?的更多相关文章

  1. 如何高效的遍历Map?你常用的不一定是最快的

    微信公众号:大黄奔跑 关注我,可了解更多有趣的面试相关问题. 写在之前 如文章标题所言,遍历Map是开发过程中比较常见的行为,实现的方式也有多种方式,本文带领大家一起看看更加高效的遍历 Map. 『茴 ...

  2. MongoDB固定集合

    固定集合 MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头 ...

  3. MongoDB固定集合(Capped Collections)

    MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素! ...

  4. mongoDB之集合操作

    mongoDB之集合操作 mongoDB中的集合相当于mysql中的表. mongoDB中集合的创建: 第一种方式:不限制集合大小   db.createCollection("集合名称&q ...

  5. MongoDB 固定集合

    MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素! ...

  6. mongoDB 删除集合后,空间不释放

    mongoDB 删除集合后,空间不释放,添加新集合,没有重新利用之前删除集合所空出来的空间,也就是数据库大小只增不减. 方法有: 1.导出导入 dump & restore 2.修复数据库 r ...

  7. C#中遍历各类数据集合的方法总结

    C#中遍历各类数据集合的方法总结: 1.枚举类型 //遍历枚举类型Sample的各个枚举名称 foreach (string sp in Enum.GetNames(typeof(Sample))) ...

  8. 关于mongodb删除集合后磁盘空间不释放的问题

    mongodb删除集合后磁盘空间不释放,只有用db.repairDatabase()去修复才能释放. 但是在修复的过程中如果出现了非正常的mongodb的挂掉,再次启动时启动不了的,需要先修复才可以, ...

  9. mongodb的集合操作

    MongoDB 创建集合 1.手动创建: 语法格式: db.createCollection(name, options) 参数说明: name: 要创建的集合名称 options: 可选参数, 指定 ...

随机推荐

  1. SharePoint 更改管理帐户密码步骤

    // https://wenku.baidu.com/view/0fffab761ed9ad51f01df2df.html \

  2. Java中的锁——Lock和synchronized

    上一篇Java中的队列同步器AQS 一.Lock接口 1.Lock接口和synchronized内置锁 a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同 ...

  3. PHP全栈从入门到精通1

    thinkphp框架,是一堆代码(常量,方法,和类)的集合,框架是一个半成品的应用,还包含一些优秀的设计模式. 框架的使用,代码风格不一样,维护难,项目生命周期短,功能扩展存在局限,好处为,简单,快捷 ...

  4. [Swift]LeetCode34. 在排序数组中查找元素的第一个和最后一个位置 | Find First and Last Position of Element in Sorted Array

    Given an array of integers nums sorted in ascending order, find the starting and ending position of ...

  5. Jason Wang: 结对编程 CountWord(第三次作业)

    本次作业地址: https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass1/homework/2882 学号: 201731072323 ...

  6. vue组件,可以通过npm引用的组件

    本文章通过实现一个vue-dialog的弹出层组件,然后附加说明如果发布此包到npm,且能被其他项目使用. 功能说明 多层弹出时,只有一个背景层. 弹出层嵌入内部组件. 弹出层按钮支持回调 源码下载 ...

  7. 获取Ip所在城市名与详细

    //获取ip和地理信息 string url = "http://pv.sohu.com/cityjson"; WebRequest wRequest = WebRequest.C ...

  8. qt QClipBoard

        部分思路借鉴这篇文章: Qt学习之路(55): 剪贴板操作     剪贴板,这个词相信大家都比较熟悉,比如使用offiece的时候就会有粘贴板,文本编辑的时候Ctrl+C和Ctrl+V的使用, ...

  9. asp.net core 系列 6 MVC框架路由(下)

    一.URL 生成 接着上篇讲MVC的路由,MVC 应用程序可以使用路由的 URL 生成功能,生成指向操作的 URL 链接. 生成 URL 可消除硬编码 URL,使代码更稳定.更易维护. 此部分重点介绍 ...

  10. macOS的OpenCL高性能计算

    随着深度学习.区块链的发展,人类对计算量的需求越来越高,在传统的计算模式下,压榨GPU的计算能力一直是重点. NV系列的显卡在这方面走的比较快,CUDA框架已经普及到了高性能计算的各个方面,比如Goo ...