在國慶先後看了《MongoDB實戰》,結合上半年工做中的雲數據庫的工做和本身使用mongo的一些的經驗,作一下總結。
本文來自「心譚博客」的《基礎、編碼和優化》和《進階:索引、複製和分片》,更多文章放在了Github倉庫。歡迎Star。javascript
MongoDB的特色:擴展策略、直觀的數據模型。在mongodb中,編程語言定義的對象能被「原封不變」地持久化,消除對象結構和程序映射的複雜性。html
關係型與正規化:對於關係型數據庫,數據表本質上是扁平的,所以表示多個一對多關係就須要多張表。常常用到的技術是拆表,這種技術是正規化。前端
但對於Mongo來講,文檔支持嵌套等多種格式,無需事先定義Schema。java
mysql和mongodb都支持即時查詢,不一樣的是:前者依賴正則化的模型;後者假定查詢字段是存儲與文檔中的。node
mongodb支持二級索引,是經過b-tree實現的。mysql
經過副本集的拓撲結構來提供複製功能,其目的是:提供數據的冗餘。git
副本集的主節點能接受讀寫操做,但從節點是隻讀的。主節點出問題,會自動故障轉移,選取一個從節點升級爲主節點。github
寫速度:給定時間內,數據庫能夠處理的插入、更新和刪除操做的數量。正則表達式
持久性:數據庫保持上述寫操做的結果不改變的,所用的時間長短。sql
DB領域,寫速度和持久性存在一種相反關係。很好理解,例如memcached,直接寫入內存,寫速度很是快,但同時數據徹底易失。
mongodb的寫操做,默認是fire-and-forget:經過TCP發送寫操做,不要求數據庫應答。用戶能夠開啓安全模式,保證寫操做正確無誤寫入db。而且安全模式能夠配置,用於阻塞操做。
對於高容量、低價值數據(點擊流、日誌),默認模式更優;對於重要數據,傾向於安全模式。
mongo中,Journaling日誌默認開啓。全部寫操做會被提交到一個只能追加的日誌中。以應對故障後的,重啓修復服務。
經過mongod
能夠運行核心服務器。數據文件存儲在/data/db
中。若是下載編譯mongo的源代碼,須要手動建立/data/db
,而且爲其分配權限。
其中,mongo的內存管理是由操做系統來處理的。數據文件經過mmap()
系統API,映射成系統的虛擬內存。
是基於JavaScript編寫的。因此能看到不少通用的語法,以及輸出的格式。
針對多個語言,都提供了驅動使用。而且風格幾乎保持統一的API接口。
安裝到MaxOS後,全局會多出如下命令:
mongodump
和mongorestore
:前者用BSON
格式,來備份數據庫數據。方便後者恢復。mongoexport
和mongoimport
:導入導出JSON、CSV和TSV格式數據。適用於事先沒法知曉數據結構的數據,或者數據結構常常不肯定性較大的數據。
除此以外,還適用於與分析相關的場景。mongo提供一種固定集合,經常使用於日誌,特色是分配的大小固定,相似於循環隊列。
因爲使用內存映射,32位系統只能對4GB內存尋址。一半內存被os佔用,那麼只有2GB能用來作映射文件。因此,必須部署在64位操做系統上。
mongo驅動的find方法,返回的是遊標對象,能夠理解爲迭代器的下標。在NodeJS中,它的名字和類型是Cursor
。
在Nodejs中,
主要有3個功能:
_id
字段中的默認值BSON
互換在自帶的交互式命令行中:
> id = ObjectId() ObjectId("5d9413867cc8dacf9247fe3e")
對於生成的5d9413867cc8dacf9247fe3e
:
- 5d941386 ,這4個字節是時間戳,單位秒數 - 7cc8da,機器ID - cf92,進程ID - 47fe3e,計數器
對全部的寫操做(插入、更新或刪除)都能開啓此模式。以此保證,操做必定在數據庫層面生效。
在v4.0中,以insert爲例,文檔以下:
db.collection.insert( <document or array of documents>, { writeConcern: <document>, ordered: <boolean> } )
關於 Write Concern的詳細參數,能夠看這篇文檔:https://docs.mongodb.com/manu...
其中,重要的是w
參數,它能夠指定是否使用應答寫入。目前默認是1,應答式寫入。設置爲0,則是非應答式。
設計數據庫Schema式根據數據庫特色和應用程序需求的狀況下,爲數據集選擇最佳表述的過程。
假設一個電商場景,要對一個商品doc進行設計。對於商品,它有多個分類category,所以須要一對多操做,同時,mongo不支持聯結操做(join)。
所以解決方案是,在商品的一個字段中,保存分類指針的數組。這裏的指針,就是mongo中的對象ID。
下面是一個簡單的例子:
> db.products.find() { "_id" : ObjectId("5d9423257cc8dacf9247fe41"), "categories" : [ ObjectId("5d9423017cc8dacf9247fe3f") ] } > db.categories.find({}) { "_id" : ObjectId("5d9423017cc8dacf9247fe3f"), "name" : "分類1" } { "_id" : ObjectId("5d9423037cc8dacf9247fe40"), "name" : "分類2" }
和前面的關係不一樣,這裏的「多」體如今「訂單」上。這裏的訂單中,保存着指向用戶的指針。
每一個產品會有多個評論,而每一個評論,可能會有點贊人列表。當要展現返回給前端的時候,須要獲取產品評論,而且獲取點贊人列表。
方案1:點贊人列表,保存着由指針組成的集合。能夠先查詢產品評論後,再對點贊作2次查詢。
方案2:因爲僅須要點贊人的頭像和名稱(少許信息),可使用去正規化,再也不保存指針,而是簡單信息。
上面2種方案,均可以防止重複點讚的發生。
即便使用use
切換一個新的數據庫,若是沒有insert數據,該數據庫並不會建立。
mongodb會爲數據、集合、索引進行空間分配,而且採起的是預分配的方式,每次空間不夠的時候,擴充2倍。
經過 db.stats()
能夠查看當前db的狀態,下面是一個示例:
> db.stats() { "db" : "info_keeper", "collections" : 3, "views" : 0, "objects" : 11, "avgObjSize" : 255.8181818181818, "dataSize" : 2814, // 數據庫中BSON對象實際大小 "storageSize" : 86016, // 包含了集合增加的預留空間和未分配的已刪除空間 "numExtents" : 0, "indexes" : 5, "indexSize" : 155648, // 數據庫索引大小的空間 "fsUsedSize" : 86272356352, "fsTotalSize" : 250685575168, "ok" : 1 }
一、重命名操做:
> use test > db.orders.renameCollection( "orders2014" )
二、固定集合
對應日誌統計之類的、只有最近的數據纔有價值的場景下,可使用固定集合:一旦容量到上限,後續插入會逐步覆蓋最早插入的文檔。
建立時候,須要同時指定createCollection
的capped和size參數:
db.createCollection('logs',{ capped : true, size : 5242880 })
爲了性能優化,mongo不會爲固定集合建立針對_id
的索引。同時,不能從中刪除doc,也不能執行任何更改文檔大小的更新操做。
三、鍵名選擇
慎重選擇鍵名,例如,用dob
代替date_of_birth
,一個文檔能夠省下10字節。
分頁查詢能夠經過skip
和limit
配合使用實現。
空值查詢能夠經過驅動的空值字面量實現,好比在node中,想查詢logs
中不包含name
字段的記錄:db.logs.find({ name: null })
。
減小序列化和網絡傳輸,能夠經過給定find的第二個參數,來選定數據庫返回給驅動的文檔的字段,好比:db.products.find({}, {_id: 1})
。這條命令,只返回文檔的_id
字段。
複合索引,複合索引的設定,遵循着「從準確到寬泛」的規則。好比對於訂單記錄,有着下單人和時間2個字段。應該先爲下單人字段設置索引,再爲時間字段設置索引。能夠理解爲前者是精確查找,能夠大大縮小查找結果集;後者是範圍查找。
嵌套字段查詢,對於負責對象字段的查詢,直接經過.
運算符便可。例如:db.demos.find({a: {b : 1}})
和db.demos.find({"a.b": 1})
是等效的。
MongoDB的查詢本質:實例化了一個遊標,並獲取它的結果集。
範圍操做符用法很簡單,但注意:不要在範圍查找時候誤用重複搜索鍵。
錯誤:db.users.find({age: { $gte: 0 }, age: { $lte: 30 } })
正確:db.users.find({age: {$gte: 0, $lte: 30}})
集合操做符一共有3個:$in
、$all
、$nin
。
in和nin是一對,in至關於使用多個OR操做符:db.products.find({'tags': {$in: [ObjectId('...'), ObjectId('...')]}})
all的做用屬性,必須是數組形式:db.products.find({tags: {$all: ['a', 'b']}})
⚠️注意:in和all能夠利用索引;nin不能利用索引,只能使用集合掃描。這和BTree結構有關。
常見的有:$ne
、$not
、 $or
、 $and
、$exists
。一樣的,$ne
不能利用索引。
對於not的使用,若是使用的操做符或者正則表達式不存在否認形式,才配合not。例如大於,就有小於等於操做符。
對與or的使用,or能夠表示不一樣鍵的值的關係,而in只能表示一個鍵的值的關係。例如:db.products.find({ $or: [{ name: 'a' }, { name: 'b' }] })
對於內嵌對象匹配,用.
運算符便可,正如前面的嵌套字段查詢所述。
不推薦對於整個對象的查詢,須要嚴格保證查詢字段的順序。
若是數組中元素是基礎對象,那麼直接查詢便可。mongo識別字段是數組類型,會自動查詢字段是否位於其中。
例如:
> db.products.insert({tags: ['a', 'b']}) WriteResult({ "nInserted" : 1 }) > db.products.find({tags: 'a'}) { "_id" : ObjectId("5d948025da0946c664997712"), "tags" : [ "a", "b" ] }
若是數組中元素是負責對象,能夠藉助.
運算符進行訪問:
> db.products.insert({address: [{name: 'home'}]}) WriteResult({ "nInserted" : 1 }) > db.products.find({"address.name": 'home'}) { "_id" : ObjectId("5d948055da0946c664997713"), "address" : [ { "name" : "home" } ] }
一樣地,你也能夠指定針對特定順序的數組元素:
> db.products.find({"address.0.name": 'home'}) { "_id" : ObjectId("5d948055da0946c664997713"), "address" : [ { "name" : "home" } ] }
若是要同時將多個條件限制在同一個子文檔上,下面是錯誤和正確的作法👇
錯誤:db.products.find({"address.name": 'home', 'address.state': 'NY'})
正確:db.products.find({address: {$elemMatch: {name: 'home', state: 'NY'} }})
對於一些複雜查詢,藉助$where
可使用js表達式。仍是以剛纔的數據爲例:
> db.products.find({$where: "function() {return this.address && this.address.length}" }) { "_id" : ObjectId("5d948055da0946c664997713"), "address" : [ { "name" : "home" } ] }
在使用的時候,須要啓動js解釋器和上下文,所以開銷大。在使用的時候,儘可能帶上其餘標準查詢操做,來縮小查詢範圍。
除此以外,還有注入攻擊的可能。主要體如今驅動使用時候,若是後端傳給db的字段是沒作檢驗的,可能發生注入攻擊。
主要體如今驅動使用上。
若是支持js的正則,那麼能夠: find({text: /best/i})
若是不支持,那麼:find({text: {$regex: 'best', $options: 'i'}})
經過$type
,能夠根據指定字段類型進行查詢。不一樣的值,表明不一樣的類型。請見官方文檔。
一、使用選擇字段進行返回,下降網絡傳輸:find
給定第二個參數。
二、返回保存在結果數組中的某個範圍的值:$slice([start, limit])
。例如:db.products.find({}, { comments: {$slice: 12}})
可以對多個字段進行升序/降序排列。例如:db.comments.find().sort({rating: -1, votes: -1})
若是向skip傳入很大的值,須要掃描同等數量的文檔,浪費資源。
最好的方法是:經過查詢條件,縮小要掃描的文檔。
在v2的版本中,mongo只能經過map、reduce等基礎操做來支持聚合搜索。但在v3的版本後,mongo自己提供了豐富的聚合階段(aggregation pipeline)和聚合運算符(aggregation operator)。
以$group
和$sum
爲例,插入了a和b兩種售賣貨物以及價錢:
> db.sales.find() { "_id" : ObjectId("5d98ca8094ffea590a8a85c6"), "name" : "a", "coin" : 100 } { "_id" : ObjectId("5d98ca8694ffea590a8a85c7"), "name" : "a", "coin" : 200 } { "_id" : ObjectId("5d98ca9094ffea590a8a85c8"), "name" : "b", "coin" : 800 }
利用聚合操做,就能夠便捷算出每種貨物的總價:
> db.sales.aggregate([{ $group: { _id: "$name", total: { $sum: "$coin" } } }]) { "_id" : "b", "total" : 800 } { "_id" : "a", "total" : 300 }
最後說一下,聚合的意義在於數據庫提供給使用者此種功能以及相關優化。固然,使用者徹底能夠在邏輯層面查詢到須要的集合,代碼中進行計算。但對於服務的提供商,完整的服務是必不可少的。
文檔更新分爲:替換更新和針對性更新。相較而言,針對性更新具備性能好、傳輸數據少和容許原子性更新的優勢。
利用$set
和$push
能夠針對文檔和其中的數組字段進行鍼對性更新,下面是針對性更新的例子:
db.products.update( { _id: 100 }, { $set: { quantity: 500 } } )
若是是替換更新,遇到增長計數器值之類的場景,在不使用樂觀鎖的狀況下,沒法保證原子性更新。由於須要先讀出數據,而後再更新。此過程當中,可能會有其餘併發程序重寫字段,從而形成髒數據。
以更新計數器的針對性更新爲例:
db.products.update( { sku: "abc123" }, { $inc: { quantity: -1 } } )
對於一些常見的結果,好比:總數、平均值等。爲了不每次都從新聚合運算,能夠在文檔中保存額外的字段緩存相關數據。
以後的業務查詢,僅僅須要查詢一次便可。
$
操做符做用:肯定數組中一個要被更新的元素的位置,而不用具體指定該元素在數組中的位置。
以下所示,不須要知道在grades數組中匹配的具體位置,用$
指代便可:
> db.students.insert([ { "_id" : 1, "grades" : [ 85, 80, 80 ] } ]) > db.students.updateOne( { _id: 1, grades: 80 }, { $set: { "grades.$" : 82 } } )
upsert
操做符做用:若是不存在,則會自動insert
。
對於添加到商品到購物車等場景,很是適用。
這裏主要使用的是findAndModify
命令。這個命令,支持傳入query參數,來作匹配篩選;支持update,來作針對性更新(原子更新)。最重要的特性是:能夠根據new
參數,來返回更新先後的文檔數據狀態。
藉助能夠返回更新文檔數據的特性,能夠mock一下mongo 4.0以前不支持的事務特性。思路是:
在MongoDB 4.0中,就是經過相似第二步的思路,提供了一個seesionID來實現了事務的,保證了事務特性。
update:multi參數不給,默認只更新匹配到的第一個文檔。
unset:刪除文檔中的指定鍵。
rename:重命名鍵。
addToSet:數組中不存在時候,纔會加入。
pull:刪除數組指定位置的元素。
更新分爲3種:
$inc
操做符$push
查詢是很是高頻的操做,大數據、高頻讀的場景下,查詢的效率會是性能的瓶頸。設置合適的索引,能夠充分利用數據結構(B數)和物理硬件的優點。
一個查詢中,要是有多個字段,好比2個字段。分離索引是:查找每一個索引的匹配集合,取得這些匹配集合的交集。複合索引是:逐步根據索引的順序作查詢。
好比有一個食譜,咱們根據種類和菜名來作索引:
肉類 - 辣子雞:第12頁 - 魚肉:第139頁
⚠️:複合索引中的順序是很是重要的,若是設置的索引不合適,那麼就至關於現行掃描文檔。抽象來講,若是有一個針對a-b的複合索引,那麼僅針對a的索引就是冗餘的。好比例子中,僅針對種類的索引就是冗餘的,可是種類索引能夠下降掃描時間(和Btree有關)。
正確的索引,也不必定會有快速的查詢:索引和數據集沒法所有放入內存。
若是內存充足,全部使用的數據文件都會載入內存,對應內存發生變化時(好比寫操做),結果會異步刷到磁盤上。
若是內存不足,就沒法所有裝入內存,出現頁錯誤,操做系統會頻繁訪問磁盤讀取須要數據。數據集過大時候,任何寫操做都要去磁盤,會出現顛簸狀況,性能下滑。
所以,應該首先保證索引都能裝入內存,複合索引時,儘可能減小鍵的數量。
在Mongo Version2中,B樹僅用於索引。集合存儲是雙向列表。
對於複合索引的底層結構,如下面爲例,是根據姓、名和生日來創建的複合索引。若是要查詢(Akroyd, Kirsten, 1978-11-02)的數據,那麼會先按照順序查找,根據第一個索引,找到了只有左側兩個複合要求;再在左側兩個集合中查找第二個索引;直到找到符合要求的數據爲止。
參考連接:
根據索引設置時的屬性的不一樣,常見的有:惟一性索引、稀疏索引和多鍵索引。
惟一性索引
說明:被設置爲索引的字段,不能重複出現,不然會報錯。
建立方式:db.col.createIndex({name: 1}, {unique: true})
。
⚠️:適用於插入數據前先建立索引的狀況
說明:索引默認是密集型的,是指爲集合中每一個文檔都創建索引。例如前面的例子,即便文檔沒有name字段,那麼查詢索引時候,沒有name字段的文檔匹配null便可。
建立方式:db.col.createIndex({name: 1}, {sparse: true})
優勢:
說明:在數組字段上創建索引。mongo中,多鍵索引是默認開啓的。
原理:數組中每一個元素,都指向文檔。
分爲爲索引值排序、排序值插入索引中,而且會佔用寫鎖,其餘程序沒法讀寫數據庫。
在遷移歷史數據和索引的時候,先遷移數據再構建集合,比線構建集合再遷移數據 的作法更優秀。
設置background爲true。雖然仍會佔用寫鎖,但會停下來,讓其餘操做讀寫操做訪問數據庫。適合在流量最低的時候,完成索引構建。
mongodump和mongorestore只能保存集合和索引說明。
若是想備份索引,必須直接備份mongo的數據文件。
對於刪除大量數據,可能形成索引碎片化。解決方法是重建索引或者執行db.col.reIndex()
.
應該在子節點執行此命令,再進行節點替換,由於它會佔用寫鎖,形成沒法讀寫操做。
兩個比較重要的原理,一個是覆蓋索引,一個複合鍵的順序。
覆蓋索引是指:查詢的關鍵字和索引徹底一致。
複合鍵的順序遵循:搜索成本由低到高的原則排列。
定義:在多臺服務器上分佈並管理數據庫服務器。有2種複製風格:主從複製和副本集(生產環境推薦)。
複製的做用是冗餘,由於複製是異步的,所以任何節點的延遲都不會影響主節點性能。
副本不是備份替代品:備份是某事刻的快照;副本是最新的。
做用:故障轉移、均衡讀負載。
最小的副本集由3個節點組成:主節點、從節點、仲裁節點。主從節點是一等的;仲裁節點不復制數據,中立觀察。
副本集基於兩個機制:oplog和心跳。oplog是記錄數據的變動;心跳是檢測主節點是否有效。
在副本集中,「提交」是指:數據變更都被複制到從節點。不然就是未提交。
不推薦,副本集纔是正道,緣由以下:
寫關注:設置writeConcern
參數,經過屬性設置來指定wtimeout、w。
讀拓展:單臺服務器沒法承受程序的讀負載,將查詢分配到副本上。
這是一個有意思的概念,尤爲是「複製」對比的時候。複製是指數據都保存在單機上,向其餘副本遷移,理論上全部主從節點數據是一致的;分片是指因爲空間有限,單機承受不了數據量,同一個數據庫分佈在不一樣的數據庫上,這些數據庫造成了一個宏觀意義上的節點。
值得稱讚的是,mongo提供分片機制,無需變更代碼。
做爲中前臺開發,開發中幾乎接觸不到複製、分片和部署的邏輯。專業事還得交給專業的人來作,畢竟每一個人精力有限,不能面面俱到。但瞭解複製和分片的原理,有助於加深對mongo的理解,也可能會在之後作架構的時候發揮做用。