《MongoDB實戰》讀書筆記

在國慶先後看了《MongoDB實戰》,結合上半年工做中的雲數據庫的工做和本身使用mongo的一些的經驗,作一下總結。

本文來自「心譚博客」《基礎、編碼和優化》《進階:索引、複製和分片》,更多文章放在了Github倉庫歡迎Starjavascript

MongoDB特性和介紹

1. 簡介

MongoDB的特色:擴展策略、直觀的數據模型。在mongodb中,編程語言定義的對象能被「原封不變」地持久化,消除對象結構和程序映射的複雜性。html

2. 主要特性

數據模型

關係型與正規化:對於關係型數據庫,數據表本質上是扁平的,所以表示多個一對多關係就須要多張表。常常用到的技術是拆表,這種技術是正規化前端

但對於Mongo來講,文檔支持嵌套等多種格式,無需事先定義Schemajava

即時查詢

mysql和mongodb都支持即時查詢,不一樣的是:前者依賴正則化的模型;後者假定查詢字段是存儲與文檔中的。node

二級索引

mongodb支持二級索引,是經過b-tree實現的。mysql

複製

經過副本集的拓撲結構來提供複製功能,其目的是:提供數據的冗餘git

副本集的主節點能接受讀寫操做,但從節點是隻讀的。主節點出問題,會自動故障轉移,選取一個從節點升級爲主節點。github

寫速度和持久性

寫速度:給定時間內,數據庫能夠處理的插入、更新和刪除操做的數量。正則表達式

持久性:數據庫保持上述寫操做的結果不改變的,所用的時間長短。sql

DB領域,寫速度和持久性存在一種相反關係。很好理解,例如memcached,直接寫入內存,寫速度很是快,但同時數據徹底易失。

mongodb的寫操做,默認是fire-and-forget:經過TCP發送寫操做,不要求數據庫應答。用戶能夠開啓安全模式,保證寫操做正確無誤寫入db。而且安全模式能夠配置,用於阻塞操做

對於高容量、低價值數據(點擊流、日誌),默認模式更優;對於重要數據,傾向於安全模式。

mongo中,Journaling日誌默認開啓。全部寫操做會被提交到一個只能追加的日誌中。以應對故障後的,重啓修復服務。

數據庫擴展

  • 垂直擴展(向上擴展):升級硬件,來提升單點性能
  • 水平擴展(向外擴展):將數據庫分佈到多臺機器,是基於自動分片。其中,單獨的分片由一個副本集組成,至少有2個節點,保證沒有單點失敗。

3. 核心服務器和工具

核心服務器

經過mongod能夠運行核心服務器。數據文件存儲在/data/db中。若是下載編譯mongo的源代碼,須要手動建立/data/db,而且爲其分配權限。

其中,mongo的內存管理是由操做系統來處理的。數據文件經過mmap()系統API,映射成系統的虛擬內存。

命令行

是基於JavaScript編寫的。因此能看到不少通用的語法,以及輸出的格式。

數據庫驅動

針對多個語言,都提供了驅動使用。而且風格幾乎保持統一的API接口。

命令行工具

安裝到MaxOS後,全局會多出如下命令:

  • mongodumpmongorestore:前者用BSON格式,來備份數據庫數據。方便後者恢復。
  • mongoexportmongoimport:導入導出JSON、CSV和TSV格式數據。

4. Mongo的場景

適用於事先沒法知曉數據結構的數據,或者數據結構常常不肯定性較大的數據。

除此以外,還適用於與分析相關的場景。mongo提供一種固定集合,經常使用於日誌,特色是分配的大小固定,相似於循環隊列。

5. 侷限

因爲使用內存映射,32位系統只能對4GB內存尋址。一半內存被os佔用,那麼只有2GB能用來作映射文件。因此,必須部署在64位操做系統上

程序編寫基礎

mongo驅動的find方法,返回的是遊標對象,能夠理解爲迭代器的下標。在NodeJS中,它的名字和類型是Cursor

在Nodejs中,

1. 驅動工做原理

主要有3個功能:

  1. 生成MongoDB對象的ID,它是存儲在_id字段中的默認值
  2. 驅動會把特定語言的文檔表述,和BSON互換
  3. 使用TCP套接字與數據庫通訊

對象ID

在自帶的交互式命令行中:

> id = ObjectId()
ObjectId("5d9413867cc8dacf9247fe3e")

對於生成的5d9413867cc8dacf9247fe3e:

- 5d941386 ,這4個字節是時間戳,單位秒數
- 7cc8da,機器ID
- cf92,進程ID
- 47fe3e,計數器

2. 安全寫入模式(Write Concern)

對全部的寫操做(插入、更新或刪除)都能開啓此模式。以此保證,操做必定在數據庫層面生效。

在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,則是非應答式。

面向文檔的數據

1. Schema 設計原則

設計數據庫Schema式根據數據庫特色和應用程序需求的狀況下,爲數據集選擇最佳表述的過程。

2. 設計電子商務數據模型

一對多:產品和分類

假設一個電商場景,要對一個商品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種方案,均可以防止重複點讚的發生。

3. 具體細節

數據庫

即便使用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字節。

查詢和聚合

1. 查詢常見技巧

分頁查詢能夠經過skiplimit配合使用實現。

空值查詢能夠經過驅動的空值字面量實現,好比在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}) 是等效的。

2. 常見查詢語言

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'} }})

Javascript查詢

對於一些複雜查詢,藉助$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,能夠根據指定字段類型進行查詢。不一樣的值,表明不一樣的類型。請見官方文檔。

3. 查詢選項

投影

一、使用選擇字段進行返回,下降網絡傳輸:find給定第二個參數。

二、返回保存在結果數組中的某個範圍的值:$slice([start, limit])。例如:db.products.find({}, { comments: {$slice: 12}})

排序

可以對多個字段進行升序/降序排列。例如:db.comments.find().sort({rating: -1, votes: -1})

skip和limit

若是向skip傳入很大的值,須要掃描同等數量的文檔,浪費資源。

最好的方法是:經過查詢條件,縮小要掃描的文檔。

4. 聚合指令

在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 }

最後說一下,聚合的意義在於數據庫提供給使用者此種功能以及相關優化。固然,使用者徹底能夠在邏輯層面查詢到須要的集合,代碼中進行計算。但對於服務的提供商,完整的服務是必不可少的。

更新、原子操做與刪除

1. 文檔更新入門

文檔更新分爲:替換更新和針對性更新。相較而言,針對性更新具備性能好、傳輸數據少和容許原子性更新的優勢。

利用$set$push能夠針對文檔和其中的數組字段進行鍼對性更新,下面是針對性更新的例子:

db.products.update(
   { _id: 100 },
   { $set:
      {
        quantity: 500
      }
   }
)

若是是替換更新,遇到增長計數器值之類的場景,在不使用樂觀鎖的狀況下,沒法保證原子性更新。由於須要先讀出數據,而後再更新。此過程當中,可能會有其餘併發程序重寫字段,從而形成髒數據。

以更新計數器的針對性更新爲例:

db.products.update(
   { sku: "abc123" },
   { $inc: { quantity: -1 } }
)

2. 電子商務數據模型中的更新

冗餘字段設計

對於一些常見的結果,好比:總數、平均值等。爲了不每次都從新聚合運算,能夠在文檔中保存額外的字段緩存相關數據。

以後的業務查詢,僅僅須要查詢一次便可。

$操做符

做用:肯定數組中一個要被更新的元素的位置,而不用具體指定該元素在數組中的位置。

以下所示,不須要知道在grades數組中匹配的具體位置,用$指代便可:

> db.students.insert([
   { "_id" : 1, "grades" : [ 85, 80, 80 ] }
])
> db.students.updateOne(
   { _id: 1, grades: 80 },
   { $set: { "grades.$" : 82 } }
)

upsert操做符

做用:若是不存在,則會自動insert

對於添加到商品到購物車等場景,很是適用。

3. 事務性工做流

這裏主要使用的是findAndModify命令。這個命令,支持傳入query參數,來作匹配篩選;支持update,來作針對性更新(原子更新)。最重要的特性是:能夠根據new參數,來返回更新先後的文檔數據狀態

藉助能夠返回更新文檔數據的特性,能夠mock一下mongo 4.0以前不支持的事務特性。思路是:

  1. 獲取最初的文檔數據
  2. 利用findAndModify進行鍼對性更新,更新字段中須要攜帶本次的更新標示(好比時間戳)。findAndModify操做符回返回更新後的字段。
  3. 將更新後的字段中的更新標示與本地保存的標示作對比,若是不相同,說明有別的端更新了數據,數據發生了污染,爲了保證事務原子性的特色,將文檔恢復爲第1步得到原始數據;若是相同,那麼繼續進行。

在MongoDB 4.0中,就是經過相似第二步的思路,提供了一個seesionID來實現了事務的,保證了事務特性。

4. 更多的更新命令

update:multi參數不給,默認只更新匹配到的第一個文檔。

unset:刪除文檔中的指定鍵。

rename:重命名鍵。

addToSet:數組中不存在時候,纔會加入。

pull:刪除數組指定位置的元素。

5. 更新本質和優化

更新分爲3種:

  • 只改變單值,但BSON文檔不變:$inc操做符
  • 改變文檔和結構,會重寫整個文檔:$push
  • 改變文檔形成空間不夠,所有總體遷移到新空間:提早利用填充因子來減小影響

索引與查詢優化

查詢是很是高頻的操做,大數據、高頻讀的場景下,查詢的效率會是性能的瓶頸。設置合適的索引,能夠充分利用數據結構(B數)和物理硬件的優點。

1. 索引理論

複合索引和分離索引

一個查詢中,要是有多個字段,好比2個字段。分離索引是:查找每一個索引的匹配集合,取得這些匹配集合的交集。複合索引是:逐步根據索引的順序作查詢。

好比有一個食譜,咱們根據種類和菜名來作索引:

肉類
    - 辣子雞:第12頁
    - 魚肉:第139頁

⚠️:複合索引中的順序是很是重要的,若是設置的索引不合適,那麼就至關於現行掃描文檔。抽象來講,若是有一個針對a-b的複合索引,那麼僅針對a的索引就是冗餘的。好比例子中,僅針對種類的索引就是冗餘的,可是種類索引能夠下降掃描時間(和Btree有關)。

索引效率

正確的索引,也不必定會有快速的查詢:索引和數據集沒法所有放入內存

若是內存充足,全部使用的數據文件都會載入內存,對應內存發生變化時(好比寫操做),結果會異步刷到磁盤上。

若是內存不足,就沒法所有裝入內存,出現頁錯誤,操做系統會頻繁訪問磁盤讀取須要數據。數據集過大時候,任何寫操做都要去磁盤,會出現顛簸狀況,性能下滑。

所以,應該首先保證索引都能裝入內存,複合索引時,儘可能減小鍵的數量。

B樹

在Mongo Version2中,B樹僅用於索引。集合存儲是雙向列表。

對於複合索引的底層結構,如下面爲例,是根據姓、名和生日來創建的複合索引。若是要查詢(Akroyd, Kirsten, 1978-11-02)的數據,那麼會先按照順序查找,根據第一個索引,找到了只有左側兩個複合要求;再在左側兩個集合中查找第二個索引;直到找到符合要求的數據爲止。

image-20191011110751982

參考連接:

2. 索引實踐

索引類型

根據索引設置時的屬性的不一樣,常見的有:惟一性索引、稀疏索引和多鍵索引。

惟一性索引

說明:被設置爲索引的字段,不能重複出現,不然會報錯。

建立方式:db.col.createIndex({name: 1}, {unique: true})

⚠️:適用於插入數據前先建立索引的狀況

稀疏索引

說明:索引默認是密集型的,是指爲集合中每一個文檔都創建索引。例如前面的例子,即便文檔沒有name字段,那麼查詢索引時候,沒有name字段的文檔匹配null便可。

建立方式:db.col.createIndex({name: 1}, {sparse: true})

優勢:

  • 佔用較少的空間
  • 適用於不是爲全部文檔增長惟一性索引
  • 適用於歷史遺留的文檔,沒法保證字段存在

多鍵索引

說明:在數組字段上創建索引。mongo中,多鍵索引是默認開啓的。

原理:數組中每一個元素,都指向文檔。

3. 查詢管理

構建索引

分爲爲索引值排序、排序值插入索引中,而且會佔用寫鎖,其餘程序沒法讀寫數據庫。

在遷移歷史數據和索引的時候,先遷移數據再構建集合,比線構建集合再遷移數據 的作法更優秀。

後臺索引

設置background爲true。雖然仍會佔用寫鎖,但會停下來,讓其餘操做讀寫操做訪問數據庫。適合在流量最低的時候,完成索引構建。

備份

mongodump和mongorestore只能保存集合和索引說明。

若是想備份索引,必須直接備份mongo的數據文件。

壓緊刪除

對於刪除大量數據,可能形成索引碎片化。解決方法是重建索引或者執行db.col.reIndex().

應該在子節點執行此命令,再進行節點替換,由於它會佔用寫鎖,形成沒法讀寫操做。

4. 查詢優化

兩個比較重要的原理,一個是覆蓋索引,一個複合鍵的順序

覆蓋索引是指:查詢的關鍵字和索引徹底一致。

複合鍵的順序遵循:搜索成本由低到高的原則排列。

複製

1. 複製概述

定義:在多臺服務器上分佈並管理數據庫服務器。有2種複製風格:主從複製和副本集(生產環境推薦)。

複製的做用是冗餘,由於複製是異步的,所以任何節點的延遲都不會影響主節點性能。

副本不是備份替代品:備份是某事刻的快照;副本是最新的。

做用:故障轉移、均衡讀負載。

2. 副本集

最小的副本集由3個節點組成:主節點、從節點、仲裁節點。主從節點是一等的;仲裁節點不復制數據,中立觀察。

image-20191011153854119

副本集基於兩個機制:oplog和心跳。oplog是記錄數據的變動;心跳是檢測主節點是否有效。

在副本集中,「提交」是指:數據變更都被複制到從節點。不然就是未提交。

3. 主從複製

不推薦,副本集纔是正道,緣由以下:

  • 故障轉移手動操做(沒有仲裁節點)
  • oplog近存在主節點,恢復苦難

4. 寫關注和讀拓展

寫關注:設置writeConcern參數,經過屬性設置來指定wtimeout、w。

讀拓展:單臺服務器沒法承受程序的讀負載,將查詢分配到副本上。

分片

這是一個有意思的概念,尤爲是「複製」對比的時候。複製是指數據都保存在單機上,向其餘副本遷移,理論上全部主從節點數據是一致的;分片是指因爲空間有限,單機承受不了數據量,同一個數據庫分佈在不一樣的數據庫上,這些數據庫造成了一個宏觀意義上的節點。

image-20191011155847647

值得稱讚的是,mongo提供分片機制,無需變更代碼。

最後

做爲中前臺開發,開發中幾乎接觸不到複製、分片和部署的邏輯。專業事還得交給專業的人來作,畢竟每一個人精力有限,不能面面俱到。但瞭解複製和分片的原理,有助於加深對mongo的理解,也可能會在之後作架構的時候發揮做用。

相關文章
相關標籤/搜索