除了特殊注釋外,本文的測試結果均基於 spring-data-mongodb:1.10.6.RELEASE(spring-boot-starter:1.5.6.RELEASE),MongoDB 3.0.6
我們在學習了一門編程語言時,一定要明白語句底層的意義,比如 User user= new User(); 它在堆中開辟了一個空間用於存放User(),並且在棧中新增了一個指向這個堆空間的指針user。那么,mongo shell中的 var user = db.user.find(); 到底做了什么?也是為集合user開辟了一個堆空間,然后再讓user指向這個空間嗎?
讓我們先來做個實驗
> function testTime(){ ... var date1 = new Date().getTime(); ... for(var i = 0; i < 10000; i++){ ... var user = db.user.find(); ... } ... return new Date().getTime() - date1; ... } > testTime(); 165
user表中是有100w條數據的,100萬條數據的空間創建10000次,只用了165ms?
顯然是不現實的,我們再看一下
> function testTime(){ var date1 = new Date().getTime(); for(var i = 0; i < 100; i++){ var user = db.user.aggregate(); } return new Date().getTime() - date1; } > testTime(); 2800
這里我們將find方法替換成了aggregate,並且將10000次循環改成了100次,然后時間卻上升了到2800ms。通過第二章我們知道aggregate的底層是findOne,讓我們再回頭仔細看看findOne和find的代碼區別
> db.user.find function ( query , fields , limit , skip, batchSize, options ){ var cursor = new DBQuery( this._mongo , this._db , this , this._fullName , this._massageObject( query ) , fields , limit , skip , batchSize , options || this.getQueryOptions() ); var connObj = this.getMongo(); var readPrefMode = connObj.getReadPrefMode(); if (readPrefMode != null) { cursor.readPref(readPrefMode, connObj.getReadPrefTagSet()); } return cursor; //find方法返回的是一個游標 } > db.user.findOne function ( query , fields, options ){ var cursor = this.find(query, fields, -1 /* limit */, 0 /* skip*/, 0 /* batchSize */, options); if ( ! cursor.hasNext() ) return null; var ret = cursor.next(); if ( cursor.hasNext() ) throw Error( "findOne has more than 1 result!" ); if ( ret.$err ) throw Error( "error " + tojson( ret ) ); return ret; //findOne返回的是具體的數據 }
find和findOne主要的差別是一個返回了游標,一個返回的實際的數據,游標就是一種類似於指針的東西,只是返回了數據庫中數據的地址,沒有對數據進行復制,所以極大的提升了查詢速率。相應的,在spring-data-mongodb中的其實也有這么一對相對的方法


執行第一個方法時,平均90%的CPU占有率跑了70分鍾,而第二個方法只用了不到一秒,說明第二個並沒有直接請求全部的數據,而是返回了一個類似於指針的游標。在 spring-data-mongodb:2.1.2RELEASE中已經去除掉這個方法,那么這個方法的優缺點是啥,為什么要去掉這個方法呢?
上面的各種測試結果表明了返回游標的好處(957ms 對上 4420270ms)。當然它也存在很致命的缺點:查詢過程中若文檔被修改,可能因為空間位置不足,而移動到集合的末尾,這樣這個位置變動的文檔就可能會被讀取到兩次,造成數據的誤差。性能的提升固然重要,但是正確性才是數據庫的核心,這可能就是新版本的spring-data-mongodb去掉了該方法的原因吧。
spring-data-mongodb去掉了該方法,而在mongo shell中提供了專門的快照功能,用於避免游標可能造成的數據重復問題,使用方式:db._collection_.find().snapshot();
現在我們弄明白了游標的優點:用一個數據讀一個數據,不事先取出全部數據,減少開銷,優化查詢,還有缺點:大數據量時容易造成數據錯亂,從游標的優缺點上我們可以知道,若查詢結果只有一個,或者是必須要遍歷所有數據才能得出查詢結果時,游標是無效的。根據這個特征,我們能得出以下三種情況不應該用游標:
(1) findOne,只取一條數據,那么也就不需要返回游標了
(2) 數據庫操作命令,用戶只關注的是操作成功或失敗
(3) 分組函數,這些函數需要遍歷完所有的數據,才能得出最后的結果
巧的是,我們最開始看的源碼就是,runCommand,aggregate底層都是用的findOne,而findOne沒有用游標,直接返回了最終結果。因此,我們可以解答最開始的疑問了:
runCommand命令和查詢函數的層級關系是怎么樣的?
查詢函數中,findOne和find並不是同一流派,findOne跟runCommand的關系更親近。mongo首先根據是否應該使用游標將 find 獨立出去了,而在runCommand、findOne、aggregate中,aggregate 調用 runCommand ,runCommand調用findOne。
因此,便有了最開始的runCommand調用了findOne方法,而為了與一般的數據表findOne查詢區分,mongo就提供了一個特殊表$cmd用於執行(2)、(3)情況的函數。這個$cmd表無法插入數據,無法直接查詢數據,使用db.getCollectionNames();時也不會展示,只有使用相應的操作符的時候,可以進行相應的查詢。
在新版本中,$cmd藏的更深了,我一直糾結的雞生蛋蛋生雞的情況也不見了,我上面總結的一些情況也過時了。技術就是這樣,總是在不斷的過時,但是思維不會過時,邏輯不會過時,各位,共勉之。
目錄
一:spring-data-mongodb 使用原生aggregate語句
