聊一聊一个最基本的问题,游标的使用。可能你历来没有注意过它,但其实它在MongoDB的使用中是广泛存在的,也存在一些常见的坑须要引发咱们的注意。javascript
在写这个系列文章时,我会假设读者已经对MongoDB有了最基础的了解,所以一些基本名词和概念就不作过多的解释,请本身查阅相关资料。java
可能你觉得你并无常常在使用游标,可是其实只要在作查询,几乎时时刻刻都在用它。本质上全部查询的数据都是从游标来的。你说你用toArray()
?不存在的,它也是在遍历游标而后返回给你一个数组而已。正是由于这样,就出现了第一个问题:除非你肯定返回数据量有限,不然不要随便toArray()
。
这里说的toArray()
包括:node
toArray()
。例如: var result = db.coll.find().toArray();toArray()
。例如:var result = await db.collection("coll").find().toArray();list()
。例如:result = list(db.coll.find());toArray()
。例如:DBCursor.toArray();由于不管游标里有多少数据,toArray()
都会给你挖出来放到内存里,变成数组返回给你。慢不说,内存也占用了不少。因此在可能的状况下,仍是尽量使用hasNext()
/next()
来得更好。python
游标主要来自两个地方:mongodb
注意两者返回的虽然都是“游标”,但又是两种不一样的游标,使用上API也不彻底相同,使用的时候请先查阅API(特别是使用NodeJS之类的动态语言的时候不要想固然)。shell
说完从哪里来,下面就该说说怎么用的问题。
可能你已经从什么地方看到过getmore
,好比mongostat的结果中。getmore
的做用是从游标中提取一批数据,具体提取多少则是由batchSize
决定。
因此当程序进行查询的时候,实际上在后台发生的事情包括:数据库
batchSize
条数据并本身缓存起来;next()
方法时,从这些缓存中提取一条并返回;batchSize
条数据都返回完以后,驱动再次经过getmore
获取batchSize
条数据。咱们能够经过shell来观察这一过程:数组
先插入一批数据:缓存
use foo for(var i = 0; i < 1000; i++) { db.bar.insert({i: i}); }
强制日志记录全部操做:bash
db.setProfilingLevel(0, 0)
跟踪日志:
tail -f mongod.log
如今执行一条find
语句:
replset:PRIMARY> db.bar.find().batchSize(50);
2018-12-29T16:01:29.587+0800 I COMMAND [conn12] command test.bar appName: "MongoDB Shell" command: find { find: "bar", filter: {}, batchSize: 50.0, \$clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: "test" } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2062 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms
虽然咱们在shell中只输出了20条结果,但实际上咱们已经从这个游标中获取了50条数据(日志中的黑体部分)。因此当咱们继续遍历这个游标时是暂时不须要再次从数据库中取数据的。同时注意咱们已经有了一个游标cursor:77199395767
。
但当咱们第三次遍历20条数据时,则会出现getmore日志:
replset:PRIMARY> it
2018-12-29T16:03:46.007+0800 I COMMAND [conn12] command test.bar appName: "MongoDB Shell" command: getMore { getMore: 77199395767, collection: "bar", batchSize: 50.0, \$clusterTime: { clusterTime: Timestamp(1546070594, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, \$db: "test" } originatingCommand: { find: "bar", filter: {}, batchSize: 50.0, \$clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, \$db: "test" } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2061 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms
2018-12-29T16:03:46.010+0800 I COMMAND [conn12] command admin.\$cmd appName: "MongoDB Shell" command: replSetGetStatus { replSetGetStatus: 1.0, forShell: 1.0, \$clusterTime: { clusterTime: Timestamp(1546070624, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, \$db: "admin" } numYields:0 reslen:896 locks:{} protocol:op_msg 0ms
它经过同一个游标再次提取了50条数据供使用。当咱们用完缓存中的数据以前都是不会再看到新的getmore
指令的。
上面已经了解了游标与驱动是如何配合工做的,那么游标超时是怎么发生的呢?条件很简单,2次getmore
之间间隔了超过10分钟,即一个游标在服务端超过10分钟无人访问,则会被回收掉。这时候若是你再针对这个游标进行getmore
,就会获得游标不存在的错误(是的,超时的游标在数据库中是不存在的,你获得的错误不会是超时,而是游标不存在。为了便于理解,咱们下面仍是称之为“游标超时”)。
那么假设你经过游标读取数据的时候是为了进行一系列分析处理,那么下一次getmore
在何时发生将取决于你的应用在多长时间内消耗完了当前缓存中的数据。换句话说,你的应用处理得越慢,下一次getmore
发生的时间就越晚。不少驱动中batchSize
的默认值是1000,这也表明着你的应用必须至少可以在10分钟内处理1000条数据,不然就会获得游标超时错误。因此诸如每一条数据须要查询其余数据库1次,须要经过RESTful API到互联网上获取相关的数据,或者须要进行一系列复杂的运算,这样的场景下,问题的关键其实不在于MongoDB怎么样,而在于你的应用到底可以处理多快。
假设问题仍是发生了,你的应用遇到了游标超时错误,怎么办呢?你至少能够有如下一些选择:
getmore
天然就发生得更早;batchSize
也能够达到一样的目的;上面已经解释过,在游标超时的时候你获得的实际是“游标不存在”错误,而不是超时。那么反过来是否是也成立呢,“游标不存在”必定是超时了吗?离散数学告诉咱们,一个命题的逆命题不必定成立。事实上也是如此。“游标不存在”的另外一种可能性是有些用户热衷于在MongoDB前面加上负载均衡/自动故障恢复的软/硬件。咱们已经知道游标是存在于一台服务器上的,若是你的负载均衡毫无原则地将请求转发到任意服务器上,getmore
同时会由于找不到游标而出现“游标不存在”的错误。事实上MongoDB和其驱动自己就已经可以完成高可用和负载均衡,并不须要额外多此一举。