除了特殊註釋外,本文的測試結果均基於 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返回的是具體的資料
}

  另外,spring-data-mongodb中的其實也有這麼一對相對的方法

 

  平均90%的CPU佔有率跑了70分鐘,說明  mongoTemplate.getCollection("user").find(new BasicDBObject()) 其實也沒有直接請求全部的資料,而是返回了一個類似於指標的遊標。不過我看  spring-data-mongodb:2.1.2RELEASE中已經去除掉這個方法了。可能是因為這個方法對於查詢資料的實時性太差了吧。

  上面的各種測試結果也表明了返回cursor的好處,當然他也不可能是完美的。因為文件在變大時,可能因為空間位置不足,而移動到集合到末尾,這樣這個位置變動的文件就可能會被讀取到兩次,造成資料的誤差,這可能就是新版本的spring-data-mongodb去掉了直接獲取遊標的方法的原因吧。

  在mongo shell中甚至提供了專門的快照功能,用於避免遊標可能造成的資料重複問題,使用方式:db._collection_.find().snapshot();

  因為遊標是為了避免一次去除過多資料造成效能的浪費,所以他對一些情況是不適用的。比如

  (1) findOne,只取一條資料,那麼也就不需要返回遊標了

  (2) 資料庫操作命令,使用者只關注的是操作成功或失敗

  (3) 分組函式,這些函式需要遍歷完所有的資料,才能得出最後的結果

  因此,邊有了最開始的runCommand呼叫了findOne方法,而為了與一般的資料查詢的區分,mongo就提供了一個特殊集合$cmd用於執行(2)、(3)情況的函式。這個$cmd集合無法插入資料,無法直接查詢資料,使用db.getCollectionNames();時也不會展示,只有使用相應的操作符的時候,可以進行相應的查詢。

  在新版本中,$cmd藏的更深了,我一直糾結的雞生蛋蛋生雞的情況也不見了,我上面總結的一些情況也過時了。技術就是這樣,總是在不斷的過時,但是思維不會過時,邏輯不會過時,各位,共勉之。


 目錄

  一:spring-data-mongodb 使用原生aggregate語句

  二:mongo的runCommand與集合操作函式的關係

  三:spring-data-mongodb與mongo shell的對應關係

  四:mongo中的遊標與資料一致性的取捨