聊一聊一個最基本的問題,遊標的使用。可能你歷來沒有注意過它,但其實它在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和其驅動自己就已經可以完成高可用和負載均衡,並不須要額外多此一舉。