閱讀101 返回首頁    go 阿裏雲 go 技術社區[雲棲]


MongoDB簡單調研

背景
        一直受傳統RDB的影響,對於數據庫表的設計可能大多數開發者都形成了思維定勢。在雲計算和大數據背景下,RDBMS正在接近極限,KV存儲將受到越來越多的關注。學習NoSQL,不求能革RDBMS的命,但希望在設計思路上能得到一些拓寬,很多場景裏,SQL表的設計和計算語句其實蠻難受的。
       RDBMS天生不是分布式的,因其保持著ACID的特性發展至今,非常重視數據完整性,但在機器規模增長的情況下,ACID是不可擴展的。同時,隨著數據量和訪問頻率增加,ACID所要維護的開銷在增大。分割數據庫,無論水平還是垂直,都是在分散總數據和讀取需求,達到優化目的,維護代價和難度也隨之上升。而KV的查找本質是散列表,且數據量無論如何增大,查找時間幾乎固定不變,即非常適合大規模數據。ACID很注重CAP中的C,而參考現實世界中很多事務,比如快遞,從你下單、付款到取貨,資金和物品的流轉並不嚴格一致,隻要在一段時間內整個交易的最後結果滿足一致性就可以了。同樣,NoSQL和RDBMS比,更偏向於BASE(Basically Available, Soft-state, Eventually consistent)的折中,重視可用性,但不追求狀態的嚴密性,且滿足最終一致性。下麵我以mongodb為例,展現一些他的特性和場景,期待NoSQL在當下能被更多的開發者拿來一顯身手。

mongodb與RDBMS
        mongodb是麵向文檔的nosql,CouchDB則是這一類數據庫的元祖。從總體上看,
1. mongodb是最親和RDBMS的一個NoSQL,能解決大部分關係型數據庫解決的問題
2. 跟麵向列存儲的HBase相比,麵向文檔存儲和麵向行存儲更接近,比如在沒有索引的情況下,掃描整個表內記錄,同樣是掃描全文檔,及文檔的每個字段
3. mongodb的索引同樣也是B樹,在一些索引的優化和設計上會和MySQL比較相似(當然需要遵循mongo的設計來做,不完全劃等號)
4. 你可以把mongodb拿RDBMS一樣來使用(當然不推薦這麼做),無非是將一行記錄變成mongodb裏的json對,在document(相當於mysql的table)之間,也可以做類似外鍵一樣的引用
5. mongodb雖然沒有嚴格的事務性操作,但是開發者自己可以做到類似事務的效果(詳見下文)。這一點也算是mongodb貼近RDBMS的一個表現吧。
6. mongo的查詢中也有mysql中的in, where, group等字段,而且想group那樣的查詢會比mysql更強大(見下文)

        以下會從各個主要關注點來展開mongo的特性,展現角度更偏向於想要調研使用mongodb的人,看看mongodb是否符合自己的業務場景,也希望我的分析會有所幫助。

存儲結構怎麼樣
        Mongodb的存儲類似JSON,每個db內有多個collection,相當於table,每個collection內是許許多多的document,這個document的schemeless的。本質上,他的麵向文檔指的是key-value中的value,而這個value可以是一個值(引用id或基本類型),可以是一個數組,也可以是一個文檔(嵌套的json對)。
        一對多是最常遇到的場景,mysql中要使用兩張或以上表的關聯甚至join進行查詢,在mongo中直接使用嵌套型或引用型(用id)就可以了。沒有特殊需求的話,嵌套的方式隻要一張"表"就可以實現。比如我這樣建立一個人的信息:
{
    id : 1,
    name : "pelick",
    hobbies : { 
        "GameA", "GameB", "GameC"
     },
     friends : {
         male : {
              2, 3, 4 # id refer to other person
         }, 
         female : {
             {
                  name : "Rita", 
                  hobbies : { "dancing" }
             }, 
             {
                   name : "Kaka",
                   nickname : "Riva"
             }
          }
      }
}
        上述這樣的結構中,展現了無模式、value為數組、嵌套、引用等。
        處理好多對多的關係可謂是NoSQL的精髓所在。理論上,可以在一個集合中完成存儲。不過實際上這樣的情況非常罕見。這是由於查詢的多樣性所導致的,若是隻有一種類型的查詢,則這種多對多的關係放在一個良好設計的集合中,雖然會有大量的冗餘,但是效率一定是最高的。如何設計這種數據庫的關鍵就是看你有多少種查詢,每一種的頻率是多少,使用的其他要求是什麼樣的。對於不同的查詢,同樣的數據庫設計的性能也是大不一樣。還有一點,一般不要拆成三個集合,這是傳統的關係型數據庫的思維方式。而常見的情況就是拆成兩個集合,然後有一部分冗餘,對最常用的查詢做一個索引。
        總結就是兩張表,一張裏麵存了另外一張裏的id集合,有冗餘存放,主要是根據查詢場景設計和建索引,不要和RDBMS一樣變三張。此外還有個好處是可以進行正反向查詢,在各自的字段裏加上id數組。

創新、更新、刪除文檔
為避免不良設計,doc大小有限製,mongodb 1.8支持16M
批量插入 mongoimport
更新:文檔替換:模式結構有大變化時,update(find_the_doc, new_doc)
          修改器:$inc 操作整數,雙精度浮點數 小範圍改動 速度很快,如果不存在則創建 {"$inc" : {"keyname" : 100 }}
                       $set $unset 修改某個鍵,如果不存在則創建
                       $push $pop 操作數組  $ne來判斷某個數組裏是否存在某值 $addToSet和$each組合
          修改器速度:$inc屬於就地修改  而數組修改器可能更改了文檔大小,會慢
                             mongodb給文檔預留了補白,來適應大小變化,如果超出了則重新分配空間,就會慢了
                             所以要自己設計。不然$push可能會成為瓶頸,就考慮把內嵌的數組獨立出來,單獨放到集合裏
          特殊的更新 upsert (update第三個參數)是原子性的,比如db.math.update({"count":25}, {"$inc":{"count":3}}, true) 如果沒有count會生成,然後繼續加3
           update的時候,第一個參數是找符合的doc,第二個參數是更新操作,第三個是否upsert,第四個參數是是否更新多個文檔
          執行完後運行getLastError看有沒有更新出錯
          findAndModify,等待數據庫相應,在操作查詢、取值和賦值整個原子性操作時很方便,速度慢些 

查詢(簡單例舉一下)
find()指定返回的鍵
查詢條件 $lt $gt $lte $gte 別的查詢鍵 $or $in $nin $not等,不具體舉例了,不是特性了解就好
查詢數組相關:$all $size $slice
正則表達式:支持Perl兼容的正則表達式(PCRE)
對查詢結果的處理 $limit $skip $sort (其中$skip適合跳過少量文檔,否則性能影響)
以上不能滿足,還有$where,更複雜的查詢是mapreduce(下文介紹)

聚合(mongodb本身的聚合操作可能可以好好依賴一下,比如olap裏複雜的查詢和本地聚合操作可以大量借用mapreduce?)
count()  distinct()  
group() 類似 group by,且可以附帶一個finalize函數對結果修剪
mapreduce可以做複雜的聚合查詢,並行化到多個服務器,當然mapreduce和group都不適合實時場景
簡單演示一下mongodb的group和mapreduce的基本寫法,不深究細節,但求了解個大概,高級查詢可以做些什麼。
Group:
> db.runCommand({"group" : {
    "ns" : "posts",
    "key" : {"tags" : true},
    "initial" : {"tags" : {}},
    "$reduce" : function(doc, prev) {
        for (var i in doc.tags) {
            if (doc.tags[i] in prev.tags) {
               prev.tags[doc.tags[i]] ++;
            } else {
               prev.tags[doc.tags[i]] = 1;
            }
         },
      "finalize" : function(prev) {
          var mostPopular = 0;
          for (i in prev.tags) {
              if (prev.tags[i] > mostPopular) {
                  prev.tag = i;
                  mostPopular = prev.tags[i];
              }
          }
          delete prev.tags
       }
}})
Mapreduce:
> map = function () {
    for (var key in this) {
        emit(key, {count : 1});
    }
};
> reduce = funtion (key, emits) {
    total = 0;
    for (var i in emits) {
        total += emits[i].count;
    }
    return {"count" : total};
}
> mr = db.runCommand({"mr_example":"test", "map":map, "reduce":reduce})
函數分為map, reduce,還有finalize,query,sort,scope等函數輔助MR。
利用好以上的查詢和聚合方法,加上合理設計,應該滿足大部分查詢場景。

索引怎麼樣
  1. 和傳統RDBMS幾乎一樣,好處是同樣的索引優化方法適用於mongodb
  2. 被索引之後,會按照索引排序
  3. 查詢一半以上的結果,不用索引,表掃描就可以了,比如查某個布爾或存在某個鍵
  4. 默認最大索引個數64,每次插入更新刪除都會產生額外開銷
  5. 用ensureIndex:1 or -1來控製索引方向
  6. 對內嵌文檔中的鍵建索引和普通索引沒有什麼區別
  7. 用.explain()看查找情況,有沒有用索引,掃描了多少文檔等等
  8. 還有基於地理空間的索引,$near :[-40, 73]  $within $box / $center
索引補充:對沒有索引的鍵使用sort,mongo會把所有數據拿到內存排序,所以有上限,不能做T級別sort。所以可以為排序而建索引。

mongodb與js
        cmd環境下是js引擎,記得使用的是V8引擎(可能隨著版本更新引擎會改變)。所有的執行語句都有一套js的API。同時,db.eval()可以執行js。更令人驚喜的是,system.js裏可以存儲js變量,所以也可以直接存儲js函數,然後用db.eval()執行調用。比如:
db.system.js.insert({"_id" : "log", "value" :
    function(msg, level) {
        var levels = [“DEBUG”, "WARN", "ERROR", "FATAL"];
        level = level ? level : 0;
        var now = new Date();
        print(now + " " + levels[level] + msg);
    });
        然後調用它,
db.eval("x = 1; log('x is  '+x); x = 2; log('x is greater than 1', 1);");
        輸出結果是:
DEBUG x is 1
WARN x is greater than 1
        我對mongodb的這一特性充滿遐想,可能很多腳本和日常操作,包括運維操作,都可以用js寫好並存儲,而且js本身可以做一切mongodb的操作,這個特性可以給我們帶來更多的什麼呢?我覺得mongodb這樣的設計,和以前我們接觸的mysql這樣的數據庫有一個很本質的區別。我們通常意義中的數據庫隻負責存儲數據,除了sql提供一些查詢和增刪改查外,其餘複雜的數據處理都需要通過各自語言的driver來連接數據庫並自己用代碼實現一些處理邏輯,最後再返回到數據庫中。而mongodb,不但提供了可以任意執行js的環境,已經mapreduce這樣複雜的查詢設計,而且還支持保存js代碼在system.js中,其實僅僅靠mongodb本身,就可以做任何你想做的數據操作和處理。這樣的好處是,使用mongodb的應用層可以完全解耦對mongodb數據處理的依賴,可能mongodb的DBA可以除了專職維護外,用一些內置的js代碼完成複雜的處理邏輯,讓應用層可以放心使用預備好的數據。我覺得這在易用的同時做到了真正的強大。此外,可以想象前端開發人員如果單獨設計一套係統或搭建一個網站,會很喜歡使用mongodb。

事務性怎麼樣(沒有寫要求的可以忽略這段)
        mongo的事務性操作其實蠻巧妙的,他支持一個findAndModify的操作,是一個保證原子性的操作。顧名思義,在一個操作裏先find目標文檔,然後進行修改,整個過程由mongo保證原子。具體使用方法不介紹了。
        此外還有一套樂觀的並發控製。樂觀並發控製其實是update本身,基於認為“同一個文檔基本上不會被同時修改”這一樂觀的事務機製。整個事務的過程會分拆為以下幾個步驟,需要開發者自己來實現:
  1.        通過查詢獲得文檔
  2.       保存文檔原始值
  3.       更新得到一個新的文檔
  4.        用原始文檔當作arg1,更新文檔當作arg2進行一次update操作
  5.        若更新失敗則重複1
        這個並發控製其實就是一次文檔自身的替換,即update的目標是整個文檔。詳細的例子還有實現兩階段提交 https://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/,即用一個記錄員collection來記錄一個事務(比如A和B之間匯款)進行過程中每一步的情況,包括initial, pending, commit, done等,從而可以進行恢複和確認事務進行情況。

 分區分庫怎麼樣
        有這樣的情況,同部門各個項目中,各組的DBA會各自做各自的分庫分區,對於mysql的分區分庫不夠透明,,而在JDBC的SQL語句層(不論使用的是MyBatis還是Spring JDBC等),不能完全將讀取數據庫路由與上層應用解耦。這應該是一個常見的問題。mongo的分片讀取都由路由來完成,應用程序隻要連接一個mongos路由進行即可,路由會從配置服務mongod裏獲得各個db和collection的情況以及分片片鍵,一般推薦一個mongos對應一個應用。
        
        上圖是一種健壯的分片部署方式。黃色mongod為配置服務器,綠色mongos為路由,藍色為真實存儲數據的分片。且每個shard裏有三個mongod,構成的是一個副本集(自動故障恢複的主從集群,且沒有固定的主節點)。藍色mongod是實際存儲數據的分片,且shard之間需要手動設定負載均衡依據的片鍵,mongodb會自動做負載均衡,相關配置信息會記錄在黃色mongod配置服務器上。分片可以動態增加和減少,隻要啟動一個mongod,用命令添加到分片集群裏,他就可以一起做負載了。而每個mongod其實都是一個單獨的mongodb實例,也可以不依賴集群單獨啟動使用。應用程序通過連接路由mongos來和mongodb打交道,應該說讀取數據是比較透明。分片集群的搭建,擴容與增減分片,副本集的搭建,應用層對分片集群的使用接入等基本概念,具體可以參看我之前一篇博客https://blog.csdn.net/pelick/article/details/8644116。總之,擴容、部
署、搭建等操作是非常方便的,這也是mongodb易用性高的一個重要原因。

GridFS存儲文件
mongo自帶存儲大的二進製文件。基本思想是把打文件分成很多塊,每塊單獨作為一個文檔存。使用方麵和存取一般的document一樣,也有分片機製,不產生磁盤碎片。視覺中國用GridFS存過大量圖片。

管理和運維
./mongod --port XXX --logpath XXX 的啟動方式,也可以./mongod --config ~/.mongodb.conf 的配置文件方式啟動
比啟動端口大1000號的端口有頁麵版的管理信息查看
mongodump作備份,mongorestore恢複備份

(待補充) 



最後更新:2017-04-03 16:48:50

  上一篇:go JMX操作實例--做一回技術控
  下一篇:go 解決maven web項目倒入eclipse不出現Maven Dependencies 和 Java System Library的問題