记一次 MongoDB 中使用游标查询数据时遇到的 BUG

368 阅读3分钟

背景

准备一个脚本将历史预约记录同步至另一个 Service 并对每条记录回写一个新的状态字段。

生产环境中需要同步的数据大约在500万条左右。

使用官方MongoDB Drivers: mongodb@3.5.4

使用 limit()

一开始虽然考虑到了生产环境数据量非常大,直接find存在打爆内存问题,但是只想到了简单的limit(),每1000条预约记录同步并回写一次,不断递归。

虽然能满足需求但是转念一想总感觉有点复杂不优雅。因为同样是IO,fs模块提供了stream读写大文件来避免内存溢出,难道 MongoDB 没有提供查询大量数据的类似方案吗?

使用 Cursor

这不河里啊,所以去翻了下官方文档发现原来find方法默认返回的Cursor就是一个实现了Iterator接口的异步可迭代对象。

推荐读一下官方Corsur文档挺通俗易懂的

Cursor的工作原理与fs模块的stream类似。简单讲就是建立一个管道后,分批次的返回结果文档。

不使用Cursor

不使用Cursor

使用Cursor 不使用Cursor

图片来源www.cnblogs.com/vajoy/p/634…

根据官方文档的解释,返回的批次大小是16MB,第一次返回的批次的文档数量不超过101条。以下是原文

The MongoDB server returns the query results in batches. The amount of data in the batch will not exceed the maximum BSON document size. To override the default size of the batch, see batchSize() and limit().

Operations of type find(), aggregate(), listIndexes, and listCollections return a maximum of 16 megabytes per batch. batchSize() can enforce a smaller limit, but not a larger one.

find() and aggregate() operations have an initial batch size of 101 documents by default. Subsequent getMore operations issued against the resulting cursor have no default batch size, so they are limited only by the 16 megabyte message size.

可以通过batchSize()limit()设置批次大小。batchSize()可以往小了调但不能超过16MB。

事情发展到了这里,不出意外的话就要出意外了

使用Cursor+while来改造limt代码

80349c810ab9fe22a676785d945376d4_.png

数据库中有4条数据,limit(4)期望的输出结果应该是遍历4条数据,但是现在只输出了2条数据。。。

我新增了cursor.count()来验证获取的数据

eec4ab57d6ab97dfbcd11216f749b54e_.png

count结果是符合预期的,也就是说cursor的遍历次数少了一半

我尝试取消limit

d20ecd04c26dd4949fcab8acc6d3c373_.png

输出又符合预期了,是limit影响了next迭代?

此时我去谷歌了一番,没有找到相同问题,但是发现了另一种迭代方式,使用for await(of),抱着侥幸心理尝试了下

a2b30bca235944bfa9e1a8f1b673fd97_.png

同样limit(4)这次居然正常了。好家伙,这样我就怀疑是mongodb@3.5.4的问题了,逐换成mongodb@4.2.2

f52f2121d4674662f4e4b9b1770f3caa_.png

果然是正确的,也就是说mongodb@3.5.4下使用corsur.hasNext()方法遍历时会发生问题。

总结

经过网友提醒奇数版本mongodb是开发版,就不应该使用。这次吃到教训了,虽然主要责任不在我,因为我是半路接手的项目,为什么会使用mongodb@3.5.4已经无法追究了,但是既然接手了项目还是应该过一遍依赖包的,检查依赖包以后会列入接手项目的准备工作之一。

当时只

后来我也尝试阅读mongodb@3.5.4corsur.hasNext()源码试图找到病因,但是没有研究出个所以然,还是太菜了。。。

喜欢记得点赞哟

网友反馈关于使用了cursor为什么还要使用limit|batchSize的问题

在这次的案例中是因为代码改动时没有删除,遗留下来的。

限制每个批次返回的结果数量,其实是有意义的。

思考一个案例,当拿到数据后业务流程非常耗时,极端情况是一个批次的数据处理时间超过游标的timeout游标自动关闭了,整个任务就终止了。所以是否要使用限制,要结合自己的业务代码来平衡。

如果想深入了解cursor,可以看下这篇文章写给服务端 Node.js 开发者的 MongoDB 数据查询游标使用指南(里面的群友就是我)