本文已經收錄進 JavaGuide(「Java學習+面試指南」一份涵蓋大部分 Java 程式設計師所需要掌握的核心知識。)
少部分內容參考了 MongoDB 官方檔案的描述,在此說明一下。
MongoDB 是一個基於 分散式檔案儲存 的開源 NoSQL 資料庫系統,由 C++ 編寫的。MongoDB 提供了 面向檔案 的儲存方式,操作起來比較簡單和容易,支援「無模式」的資料建模,可以儲存比較複雜的資料型別,是一款非常流行的 檔案型別資料庫 。
在高負載的情況下,MongoDB 天然支援水平擴充套件和高可用,可以很方便地新增更多的節點/範例,以保證服務效能和可用性。在許多場景下,MongoDB 可以用於代替傳統的關係型資料庫或鍵/值儲存方式,皆在為 Web 應用提供可延伸的高可用高效能資料儲存解決方案。
MongoDB 的儲存結構區別於傳統的關係型資料庫,主要由如下三個單元組成:
也就是說,MongoDB 將資料記錄儲存為檔案 (更具體來說是BSON 檔案),這些檔案在集合中聚集在一起,資料庫中儲存一個或多個檔案集合。
SQL 與 MongoDB 常見術語對比 :
SQL | MongoDB |
---|---|
表(Table) | 集合(Collection) |
行(Row) | 檔案(Document) |
列(Col) | 欄位(Field) |
主鍵(Primary Key) | 物件 ID(Objectid) |
索引(Index) | 索引(Index) |
巢狀表(Embeded Table) | 嵌入式檔案(Embeded Document) |
陣列(Array) | 陣列(Array) |
MongoDB 中的記錄就是一個 BSON 檔案,它是由鍵值對組成的資料結構,類似於 JSON 物件,是 MongoDB 中的基本資料單元。欄位的值可能包括其他檔案、陣列和檔案陣列。
檔案的鍵是字串。除了少數例外情況,鍵可以使用任意 UTF-8 字元。
\0
(空字元)。這個字元用來表示鍵的結尾。.
和 $
有特別的意義,只有在特定環境下才能使用。_
開頭的鍵是保留的(不是嚴格要求的)。BSON [bee·sahn] 是 Binary JSON的簡稱,是 JSON 檔案的二進位制表示,支援將檔案和陣列嵌入到其他檔案和陣列中,還包含允許表示不屬於 JSON 規範的資料型別的擴充套件。有關 BSON 規範的內容,可以參考 bsonspec.org,另見BSON 型別。
根據維基百科對 BJSON 的介紹,BJSON 的遍歷速度優於 JSON,這也是 MongoDB 選擇 BSON 的主要原因,但 BJSON 需要更多的儲存空間。
與 JSON 相比,BSON 著眼於提高儲存和掃描效率。BSON 檔案中的大型元素以長度欄位為字首以便於掃描。在某些情況下,由於長度字首和顯式陣列索引的存在,BSON 使用的空間會多於 JSON。
MongoDB 集合存在於資料庫中,沒有固定的結構,也就是 無模式 的,這意味著可以往集合插入不同格式和型別的資料。不過,通常情況相愛插入集合中的資料都會有一定的關聯性。
集合不需要事先建立,當第一個檔案插入或者第一個索引建立時,如果該集合不存在,則會建立一個新的集合。
集合名可以是滿足下列條件的任意 UTF-8 字串:
""
。\0
(空字元),這個字元表示集合名的結尾。system.users
這個集合儲存著資料庫的使用者資訊,system.namespaces
集合儲存著所有資料庫集合的資訊。$
。資料庫用於儲存所有集合,而集合又用於儲存所有檔案。一個 MongoDB 中可以建立多個資料庫,每一個資料庫都有自己的集合和許可權。
MongoDB 預留了幾個特殊的資料庫。
資料庫名可以是滿足以下條件的任意 UTF-8 字串:
""
。' '
(空格)、.
、$
、/
、\
和 \0
(空字元)。資料庫名最終會變成檔案系統裡的檔案,這也就是有如此多限制的原因。
MongoDB 的優勢在於其資料模型和儲存引擎的靈活性、架構的可延伸性以及對強大的索引支援。
選用 MongoDB 應該充分考慮 MongoDB 的優勢,結合實際專案的需求來決定:
儲存引擎(Storage Engine)是資料庫的核心元件,負責管理資料在記憶體和磁碟中的儲存方式。
與 MySQL 一樣,MongoDB 採用的也是 外掛式的儲存引擎架構 ,支援不同型別的儲存引擎,不同的儲存引擎解決不同場景的問題。在建立資料庫或集合時,可以指定儲存引擎。
外掛式的儲存引擎架構可以實現 Server 層和儲存引擎層的解耦,可以支援多種儲存引擎,如MySQL既可以支援B-Tree結構的InnoDB儲存引擎,還可以支援LSM結構的RocksDB儲存引擎。
在儲存引擎剛出來的時候,預設是使用 MMAPV1 儲存引擎,MongoDB4.x 版本不再支援 MMAPv1 儲存引擎。
現在主要有下面這兩種儲存引擎:
此外,MongoDB 3.0 提供了 可插拔的儲存引擎 API ,允許第三方為 MongoDB 開發儲存引擎,這點和 MySQL 也比較類似。
目前絕大部分流行的資料庫儲存引擎都是基於 B/B+ Tree 或者 LSM(Log Structured Merge) Tree 來實現的。對於 NoSQL 資料庫來說,絕大部分(比如 HBase、Cassandra、RocksDB)都是基於 LSM 樹,MongoDB 不太一樣。
上面也說了,自 MongoDB 3.2 以後,預設的儲存引擎為WiredTiger 儲存引擎。在 WiredTiger 引擎官網上,我們發現 WiredTiger 使用的是 B+ 樹作為其儲存結構:
WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.
此外,WiredTiger 還支援 LSM(Log Structured Merge) 樹作為儲存結構,MongoDB 在使用WiredTiger 作為儲存引擎時,預設使用的是 B+ 樹。
如果想要了解 MongoDB 使用 B 樹的原因,可以看看這篇文章:為什麼 MongoDB 使用 B 樹?。
使用 B+ 樹時,WiredTiger 以 page 為基本單位往磁碟讀寫資料。B+ 樹的每個節點為一個 page,共有三種型別的 page:
其整體結構如下圖所示:
如果想要深入研究學習 WiredTiger 儲存引擎,推薦閱讀 MongoDB 中文社群的 WiredTiger儲存引擎系列。
實際專案中,我們經常需要將多個檔案甚至是多個集合彙總到一起計算分析(比如求和、取最大值)並返回計算後的結果,這個過程被稱為 聚合操作 。
根據官方檔案介紹,我們可以使用聚合操作來:
MongoDB 提供了兩種執行聚合的方法:
count()
、distinct()
、estimatedDocumentCount()
。絕大部分文章中還提到了 map-reduce 這種聚合方法。不過,從 MongoDB 5.0 開始,map-reduce 已經不被官方推薦使用了,替代方案是 聚合管道。聚合管道提供比 map-reduce 更好的效能和可用性。
MongoDB 聚合管道由多個階段組成,每個階段在檔案通過管道時轉換檔案。每個階段接收前一個階段的輸出,進一步處理資料,並將其作為輸入資料傳送到下一個階段。
每個管道的工作流程是:
常用階段操作符 :
操作符 | 簡述 |
---|---|
$match | 匹配操作符,用於對檔案集合進行篩選 |
$project | 投射操作符,用於重構每一個檔案的欄位,可以提取欄位,重新命名欄位,甚至可以對原有欄位進行操作後新增欄位 |
$sort | 排序操作符,用於根據一個或多個欄位對檔案進行排序 |
$limit | 限制操作符,用於限制返回檔案的數量 |
$skip | 跳過操作符,用於跳過指定數量的檔案 |
$count | 統計操作符,用於統計檔案的數量 |
$group | 分組操作符,用於對檔案集合進行分組 |
$unwind | 拆分操作符,用於將陣列中的每一個值拆分為單獨的檔案 |
$lookup | 連線操作符,用於連線同一個資料庫中另一個集合,並獲取指定的檔案,類似於 populate |
更多操作符介紹詳見官方檔案:https://docs.mongodb.com/manual/reference/operator/aggregation/
階段操作符用於 db.collection.aggregate
方法裡面,陣列引數中的第一層。
db.collection.aggregate( [ { 階段操作符:表述 }, { 階段操作符:表述 }, ... ] )
下面是 MongoDB 官方檔案中的一個例子:
db.orders.aggregate([
# 第一階段:$match階段按status欄位過濾檔案,並將status等於"A"的檔案傳遞到下一階段。
{ $match: { status: "A" } },
# 第二階段:$group階段按cust_id欄位將檔案分組,以計算每個cust_id唯一值的金額總和。
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])
MongoDB 事務想要搞懂原理還是比較花費時間的,我自己也沒有搞太明白。因此,我這裡只是簡單介紹一下 MongoDB 事務,想要了解原理的小夥伴,可以自行搜尋查閱相關資料。
這裡推薦幾篇文章,供大家參考:
我們在介紹 NoSQL 資料的時候也說過,NoSQL 資料庫通常不支援事務,為了可延伸和高效能進行了權衡。不過,也有例外,MongoDB 就支援事務。
與關係型資料庫一樣,MongoDB 事務同樣具有 ACID 特性:
Atomicity
) : 事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;Consistency
): 執行事務前後,資料保持一致,例如轉賬業務中,無論事務是否成功,轉賬者和收款人的總額應該是不變的;Isolation
): 並行存取資料庫時,一個使用者的事務不被其他事務所幹擾,各並行事務之間資料庫是獨立的。WiredTiger 儲存引擎支援讀未提交( read-uncommitted )、讀已提交( read-committed )和快照( snapshot )隔離,MongoDB 啟動時預設選快照隔離。在不同隔離級別下,一個事務的生命週期內,可能出現髒讀、不可重複讀、幻讀等現象。Durability
): 一個事務被提交之後。它對資料庫中資料的改變是持久的,即使資料庫發生故障也不應該對其有任何影響。關於事務的詳細介紹這篇文章就不多說了,感興趣的可以看看我寫的MySQL常見面試題總結這篇文章,裡面有詳細介紹到。
MongoDB 單檔案原生支援原子性,也具備事務的特性。當談論 MongoDB 事務的時候,通常指的是 多檔案 。MongoDB 4.0 加入了對多檔案 ACID 事務的支援,但只支援複製集部署模式下的 ACID 事務,也就是說事務的作用域限制為一個副本集內。MongoDB 4.2 引入了 分散式事務 ,增加了對分片叢集上多檔案事務的支援,併合並了對副本集上多檔案事務的現有支援。
根據官方檔案介紹:
從 MongoDB 4.2 開始,分散式事務和多檔案事務在 MongoDB 中是一個意思。分散式事務是指分片叢集和副本集上的多檔案事務。從 MongoDB 4.2 開始,多檔案事務(無論是在分片叢集還是副本集上)也稱為分散式事務。
在大多數情況下,多檔案事務比單檔案寫入會產生更大的效能成本。對於大部分場景來說, 非規範化資料模型(嵌入式檔案和陣列) 依然是最佳選擇。也就是說,適當地對資料進行建模可以最大限度地減少對多檔案事務的需求。
注意 :
藉助 WiredTiger 儲存引擎( MongoDB 3.2 後的預設儲存引擎),MongoDB 支援對所有集合和索引進行壓縮。壓縮以額外的 CPU 為代價最大限度地減少儲存使用。
預設情況下,WiredTiger 使用 Snappy 壓縮演演算法(谷歌開源,旨在實現非常高的速度和合理的壓縮,壓縮比 3 ~ 5 倍)對所有集合使用塊壓縮,對所有索引使用字首壓縮。
除了 Snappy 之外,對於集合還有下面這些壓縮演演算法:
WiredTiger 紀錄檔也會被壓縮,預設使用的也是 Snappy 壓縮演演算法。如果紀錄檔記錄小於或等於 128 位元組,WiredTiger 不會壓縮該記錄。
title: MongoDB常見面試題總結(下)
category: 資料庫
tag:
和關係型資料庫類似,MongoDB 中也有索引。索引的目的主要是用來提高查詢效率,如果沒有索引的話,MongoDB 必須執行 集合掃描 ,即掃描集合中的每個檔案,以選擇與查詢語句匹配的檔案。如果查詢存在合適的索引,MongoDB 可以使用該索引來限制它必須檢查的檔案數量。並且,MongoDB 可以使用索引中的排序返回排序後的結果。
雖然索引可以顯著縮短查詢時間,但是使用索引、維護索引是有代價的。在執行寫入操作時,除了要更新檔案之外,還必須更新索引,這必然會影響寫入的效能。因此,當有大量寫操作而讀操作少時,或者不考慮讀操作的效能時,都不推薦建立索引。
MongoDB 支援多種型別的索引,包括單欄位索引、複合索引、多鍵索引、雜湊索引、文字索引、 地理位置索引等,每種型別的索引有不同的使用場合。
複合索引中欄位的順序非常重要,例如下圖中的複合索引由{userid:1, score:-1}
組成,則該複合索引首先按照userid
升序排序;然後再每個userid
的值內,再按照score
降序排序。
在複合索引中,按照何種方式排序,決定了該索引在查詢中是否能被應用到。
走複合索引的排序:
db.s2.find().sort({"userid": 1, "score": -1})
db.s2.find().sort({"userid": -1, "score": 1})
不走複合索引的排序:
db.s2.find().sort({"userid": 1, "score": 1})
db.s2.find().sort({"userid": -1, "score": -1})
db.s2.find().sort({"score": 1, "userid": -1})
db.s2.find().sort({"score": 1, "userid": 1})
db.s2.find().sort({"score": -1, "userid": -1})
db.s2.find().sort({"score": -1, "userid": 1})
我們可以通過 explain 進行分析:
db.s2.find().sort({"score": -1, "userid": 1}).explain()
MongoDB 的複合索引遵循左字首原則 :擁有多個鍵的索引,可以同時得到所有這些鍵的字首組成的索引,但不包括除左字首之外的其他子集。比如說,有一個類似 {a: 1, b: 1, c: 1, ..., z: 1}
這樣的索引,那麼實際上也等於有了 {a: 1}
、{a: 1, b: 1}
、{a: 1, b: 1, c: 1}
等一系列索引,但是不會有 {b: 1}
這樣的非左字首的索引。
TTL 索引提供了一個過期機制,允許為每一個檔案設定一個過期時間 expireAfterSeconds
,當一個檔案達到預設的過期時間之後就會被刪除。TTL 索引除了有 expireAfterSeconds
屬性外,和普通索引一樣。
資料過期對於某些型別的資訊很有用,比如機器生成的事件資料、紀錄檔和對談資訊,這些資訊只需要在資料庫中儲存有限的時間。
TTL 索引執行原理 :
TTL 索引限制 :
_id
欄位不支援 TTL 索引。根據官方檔案介紹,覆蓋查詢是以下的查詢:
null
。由於所有出現在查詢中的欄位是索引的一部分, MongoDB 無需在整個資料檔案中檢索匹配查詢條件和返回使用相同索引的查詢結果。因為索引存在於記憶體中,從索引中獲取資料比通過掃描檔案讀取資料要快得多。
舉個例子:我們有如下 users
集合:
{
"_id": ObjectId("53402597d852426020000002"),
"contact": "987654321",
"dob": "01-01-1991",
"gender": "M",
"name": "Tom Benzamin",
"user_name": "tombenzamin"
}
我們在 users
集合中建立聯合索引,欄位為 gender
和 user_name
:
db.users.ensureIndex({gender:1,user_name:1})
現在,該索引會覆蓋以下查詢:
db.users.find({gender:"M"},{user_name:1,_id:0})
為了讓指定的索引覆蓋查詢,必須顯式地指定 _id: 0
來從結果中排除 _id
欄位,因為索引不包括 _id
欄位。
MongoDB 的複製叢集又稱為副本叢集,是一組維護相同資料集合的 mongod 程序。
使用者端連線到整個 Mongodb 複製叢集,主節點機負責整個複製叢集的寫,從節點可以進行讀操作,但預設還是主節點負責整個複製叢集的讀。主節點發生故障時,自動從從節點中選舉出一個新的主節點,確保叢集的正常使用,這對於使用者端來說是無感知的。
通常來說,一個複製叢集包含 1 個主節點(Primary),多個從節點(Secondary)以及零個或 1 個仲裁節點(Arbiter)。
下圖是一個典型的三成員副本叢集:
主節點與備節點之間是通過 oplog(操作紀錄檔) 來同步資料的。oplog 是 local 庫下的一個特殊的 上限集合(Capped Collection) ,用來儲存寫操作所產生的增量紀錄檔,類似於 MySQL 中 的 Binlog。
上限集合類似於定長的迴圈佇列,資料順序追加到集合的尾部,當集合空間達到上限時,它會覆蓋集合中最舊的檔案。上限集合的資料將會被順序寫入到磁碟的固定空間內,所以,I/O 速度非常快,如果不建立索引,效能更好。
當主節點上的一個寫操作完成後,會向 oplog 集合寫入一條對應的紀錄檔,而從節點則通過這個 oplog 不斷拉取到新的紀錄檔,在本地進行回放以達到資料同步的目的。
副本集最多有一個主節點。 如果當前主節點不可用,一個選舉會抉擇出新的主節點。MongoDB 的節點選舉規則能夠保證在 Primary 掛掉之後選取的新節點一定是叢集中資料最全的一個。
分片叢集是 MongoDB 的分散式版本,相較副本集,分片叢集資料被均衡的分佈在不同分片中, 不僅大幅提升了整個叢集的資料容量上限,也將讀寫的壓力分散到不同分片,以解決副本集效能瓶頸的難題。
MongoDB 的分片叢集由如下三個部分組成(下圖來源於官方檔案對分片叢集的介紹):
隨著系統資料量以及吞吐量的增長,常見的解決辦法有兩種:垂直擴充套件和水平擴充套件。
垂直擴充套件通過增加單個伺服器的能力來實現,比如磁碟空間、記憶體容量、CPU 數量等;水平擴充套件則通過將資料儲存到多個伺服器上來實現,根據需要新增額外的伺服器以增加容量。
類似於 Redis Cluster,MongoDB 也可以通過分片實現 水平擴充套件 。水平擴充套件這種方式更靈活,可以滿足更巨量資料量的儲存需求,支援更高吞吐量。並且,水平擴充套件所需的整體成本更低,僅僅需要相對較低設定的單機伺服器即可,代價是增加了部署的基礎設施和維護的複雜性。
也就是說當你遇到如下問題時,可以使用分片叢集解決:
分片鍵(Shard Key) 是資料分割區的前提, 從而實現資料分發到不同伺服器上,減輕伺服器的負擔。也就是說,分片鍵決定了集合內的檔案如何在叢集的多個分片間的分佈狀況。
分片鍵就是檔案裡面的一個欄位,但是這個欄位不是普通的欄位,有一定的要求:
_id
欄位,否則您可以更新檔案的分片鍵值。MongoDB 5.0 版本開始,實現了實時重新分片(live resharding),可以實現分片鍵的完全重新選擇。選擇合適的片鍵對 sharding 效率影響很大,主要基於如下四個因素(摘自分片叢集使用注意事項 - - 騰訊雲檔案):
綜上,在選擇片鍵時要考慮以上4個條件,儘可能滿足更多的條件,才能降低 MoveChuncks 對效能的影響,從而獲得最優的效能體驗。
MongoDB 支援兩種分片演演算法來滿足不同的查詢需求(摘自MongoDB 分片叢集介紹 - 阿里雲檔案):
1、基於範圍的分片 :
MongoDB 按照分片鍵(Shard Key)的值的範圍將資料拆分為不同的塊(Chunk),每個塊包含了一段範圍內的資料。當分片鍵的基數大、頻率低且值非單調變更時,範圍分片更高效。
2、基於 Hash 值的分片
MongoDB 計算單個欄位的雜湊值作為索引值,並以雜湊值的範圍將資料拆分為不同的塊(Chunk)。
除了上述兩種分片策略,您還可以設定 複合片鍵 ,例如由一個低基數的鍵和一個單調遞增的鍵組成。
Chunk(塊) 是 MongoDB 分片叢集的一個核心概念,其本質上就是由一組 Document 組成的邏輯資料單元。每個 Chunk 包含一定範圍片鍵的資料,互不相交且並集為全部資料,即離散數學中劃分的概念。
分片叢集不會記錄每條資料在哪個分片上,而是記錄 Chunk 在哪個分片上一級這個 Chunk 包含哪些資料。
預設情況下,一個 Chunk 的最大值預設為 64MB(可調整,取值範圍為 1~1024 MB。如無特殊需求,建議保持預設值),進行資料插入、更新、刪除時,如果此時 Mongos 感知到了目標 Chunk 的大小或者其中的資料量超過上限,則會觸發 **Chunk ****。
資料的增長會讓 Chunk **得越來越多。這個時候,各個分片上的 Chunk 數量可能會不平衡。Mongos 中的 均衡器(Balancer) 元件就會執行自動平衡,嘗試使各個 Shard 上 Chunk 的數量保持均衡,這個過程就是 再平衡(Rebalance)。預設情況下,資料庫和集合的 Rebalance 是開啟的。
如下圖所示,隨著資料插入,導致 Chunk **,讓 AB 兩個分片有 3 個 Chunk,C 分片只有一個,這個時候就會把 B 分配的遷移一個到 C 分片實現叢集資料均衡。
Balancer 是 MongoDB 的一個執行在 Config Server 的 Primary 節點上(自 MongoDB 3.4 版本起)的後臺程序,它監控每個分片上 Chunk 數量,並在某個分片上 Chunk 數量達到閾值進行遷移。
Chunk 只會分裂,不會合並,即使 chunkSize 的值變大。
Rebalance 操作是比較耗費系統資源的,我們可以通過在業務低峰期執行、預分片或者設定 Rebalance 時間窗等方式來減少其對 MongoDB 正常使用所帶來的影響。
關於 Chunk 遷移原理的詳細介紹,推薦閱讀 MongoDB 中文社群的一文讀懂 MongoDB chunk 遷移這篇文章。