MongoDB 為知名的 document-based NoSQL DB,提供了 schema-free 的 document 儲存,和 data sharding cluster 功能。
NoSQL 設計目的不僅是 Schema-Free,還有為了更好支援分散式架構和水平擴展(Sharding)。
SQL DB 強調正規化把資料拆成不同 Table 並透過 Join 關聯,如果資料散落在不同 DB,跨 Shard Join 查詢效能很差,而 NoSQL 強調反正規化,將高度關聯的資料放進一個 Document,有效降低跨 Shard 查詢需求。
不像 MySQL,Schema 與資料是分開儲存且 Schema 只儲存一份,MongoDB 用 Binary JSON (BSON) 格式把 Schema 跟資料一起儲存,雖會佔用較多空間,但每筆資料有自己的 Schema,修改 Schema 影響範圍小且方便,例如修改 Schema 不需要全部資料都更新。
不用 JSON 儲存是因為 JSON 是 text-based,即便只讀取部分 key 仍要解析全部內容,BSON 是 binary 格式並透過 schema 內容 skip file offset 定位 key,且 JSON 沒有紀錄每個 key 的 data type,BSON 會用額外的 type byte 紀錄資料類別,讓 MongoDB 能針對不同類別提供不同操作 (e.g push, set),因此單個 Document 可有多個操作。
雖然 MongoDB NoSQL 設計適合 data sharding 的水平擴展,但 transaction isolation 功能卻受限於分散式架構而無法提供完整的 isolation。
MongoDB 也用 MVCC 實作 Isolation,然而只提供單 Shard 的 Repeated Read 以及跨 Shard 的 Read Committed 功能,雖然保證單 Document 操作是 atomic 且不會 race condition,但不提供類似 SQL FOR UPDATE 上鎖語法。
沒有 Serializable Level 跟 FOR UPDATE
語法容易造成 Write Skew 問題:
此外跨 Shard 的 Transaction 查詢不支援 RR 也會出現 Read Skew 的問題:
RR Isolation 要在 transaction begin 時建立 snapshot,因此 Tx1 不應該讀到 Tx2 Commit 後的內容,但由於 MongoDB 無法建立跨 shard 的統一 snapshot,當 david 餘額資料在不同 shard 時,就須等查詢時才建立 snapshot,導致 david 餘額是 Tx2 Commit 後的內容,但 vic 餘額卻是 Tx2 Commit 前的內容。
FOR UPDATE
& 跨 Shard Snapshot?當 Transaction 對多筆在不同 DB (Shard) 的資料上鎖,需要跨 Server 管理鎖,例如確認上鎖是否成功,不成功要 retry,如果最終還是失敗,要 rollback 並解鎖其他原本上鎖成功的資料,管理不簡單,要在分散式 transaction 實作加上更多機制。
雖然 MongoDB 4.2 後支援 2PC 的分散式 transaction 的 atomic 功能,但 MongoDB 更推薦透過反正規化的方式,將原本對多 Table 操作改成對單 Document 的多操作,避免跨 Shard 操作影響效能。
另外要實作跨 Shard 統一 Snapshot 要建立 Global Logical Clock,Global Logical Clock 對於部署環境有高度硬體要求 (e.g 網路低延遲或原子鐘),不適合開源的 MongoDB。
反正規化將多筆資料放入同個 Document 操作,能避免跨 Shard 查詢,可提升效能,以及解決上述資料不一致問題,但並不是所有資料都能在同個 Document 操作,畢竟 Document 有容量上限,若需要對資料上鎖,雖沒有 FOR UPDATE 語法,但可參考 Redis 分散式鎖設計,用 MongoDB findOneAndUpdate 語法實現應用層鎖。
另外在 Cluster 架構,MongoDB 是有可能讀到未 Commit 的資料。當 Cluster 中的 Primary DB 掛掉,若新上任的 Primary DB 發現有資料沒同步到 Majority (e.g 只有前一個 Primary DB 寫成功的資料) 會將其 Rollback,這就導致原本從舊 Primary DB 查到的資料,在新 Primary 被 Rollback 等於查到未 Commit 的資料。
MongoDB 提供 Read & Write Concern 配置,讓用戶自行在效能跟資料一致性中做選擇,例如不想讀到 rollback 資料,可以設定 read: majority & write: majority 確保寫入要等多數 commit 以及只讀多數 commit 資料,如果想要效能最好可以設置 read: local & write:1,讀寫不等 majority commit ,但可能讀到被 Rollback 資料。
但 Read & Write Concern 設置在 Transaction 使用上有不少坑,例如: