MongoDB Write Concern 與 Read Concern 說明
Posted on Jan 11, 2024 by Amo Chen ‐ 6 min read
MongoDB Write Concern 與 Read Concern 其實是使用 MongoDB 叢集(cluster)環境必須認識的重要概念,如果不認識這 2 個重要概念,就很容易寫出不符預期的操作,甚至導致 bug 產生。
本文將盡量以淺顯易懂的方式介紹 MongoDB Write Concern 與 Read Concern 。
本文環境
- MongoDB 6.0
Distributed Transactions / 分散式交易
在談 MongoDB 的 Write Concern 與 Read Concern 之前,有必要先認識什麼是 “Distributed Transaction” 。
In MongoDB, an operation on a single document is atomic.
首先, MongoDB 已經確保對於單一文件的操作具備不可分割性,也就是 ACID 中的 A (Atomic), 代表 MongoDB 對單一文件的操作只有全部成功或者全部失敗, 2 種可能性,沒有部份成功/失敗的可能性。
p.s. 如果不熟悉什麼是 ACID 的話,詳見 後端工程師面試考什麼 - ACID 篇 。
這時就衍生 1 個問題,如果我們想同時操作 2 個文件,就沒有 Atomic 保證,那怎麼辦?
舉個儲值流程作為例子,假設使用者進行儲值時,我們會在 MongoDB 資料庫操作 2 筆資料,其中新增 1 筆資料作為儲值紀錄,另 1 筆則是更新使用者目前儲值總金額:
// JavaScript
const db = client.db("mydb")
const userId = "xyzdef"
const amount = 100
const time = new Date().getTime()
// insert the record
db.collection("deposit_records").insertOne(
{ userId, amount, time },
);
// update the total amount
db.collection("users").findOneAndUpdate(
{ userId, },
{ $inc: { amount, },},
);
如果在操作第 2 筆資料時, MongoDB 不幸因為某種原因故障,就會導致使用者看到自己有儲值紀錄,但是目前可用儲值金額卻沒有更新的情況,因為 MongoDB 只確保對單一文件的操作具備不可切割性。
為了解決這種問題, MongoDB 4.0 之後開始支援 Distributed Transactions, 也稱為 Multi-document Transactions, 用以解決操作多個文件或者 MongoDB cluster 多個資料庫之間的 Atomic Transactions 問題。
如此一來,就可以用 Tranasaction 確保前述儲值流程會全部成功或全部失敗,不會有部分成功的情況。
以下是使用 MongoDB Transaction 的 JavaScript 程式碼範例,其中 await session.withTransaction
就是使用 Transaction 最主要的部分(在此不多做解釋):
const { MongoClient } = require('mongodb');
// connection URL
const url = 'mongodb://mongo01:27021,mongo02:27022,mongo03:27023/?replicaSet=rs0&readPreference=secondary';
const client = new MongoClient(url);
// database Name
const dbName = 'mydb';
async function main() {
await client.connect();
const db = client.db(dbName);
const userId = 'xyzdef';
const amount = 100;
const time = new Date().getTime();
const session = client.startSession();
const transactionOptions = {
readPreference: 'primary',
readConcern: { level: 'local' },
writeConcern: { w: 'majority' },
};
try {
await session.withTransaction(async () => {
// Important:: You must pass the session to the operations
await db.collection('deposit_records').insertOne({ userId, amount, time }, { session });
await db.collection('users').findOneAndUpdate({ userId }, { $inc: { amount } }, { session });
}, transactionOptions);
} finally {
await session.endSession();
await client.close();
}
return 'done.';
}
main()
.then(console.log)
.catch(console.error)
.finally(() => client.close());
但是要使用 Multi-document Transactions 的話,就需要啟用 MongoDB replica sets 或者 sharded clusters 。
p.s. 關於 replication 跟 sharding 的差異可以參考 What Is Replication In MongoDB? 一文,簡單來說 replication 有複製資料到 replicas 的行為, sharding 是分散資料到不同的 nodes, 每個 node 都只有部分的資料,所有的 nodes 加起來才是全部的資料
Write Concern
初步了解 Multi-document Transactions 的由來與用途之後,對於 Write Concern 就會更好理解。
Write concern describes the level of acknowledgment requested from MongoDB for write operations to a standalone mongod or to Replica sets or to sharded clusters.
先解釋何謂 WriteConcern, 簡單來說就是 MongoDB 對於寫入操作的回應策略/方式。
舉例來說,在只有 1 台 MongoDB 的情況之下,我們發出寫入 1 筆資料的請求(request)之後,我們幾乎可以預期立馬收到來自 MongoDB 的回應,告訴我們寫入成功,但在多台 MongoDB 叢集的架構之下,當我們同樣發出寫入 1 筆資料的請求時, MongoDB 要等到何時才能回應我們寫入成功?是只要任何 1 台寫入資料成功就可以回應?還是要 50% 以上的 MongDB nodes 都寫入成功才回應?
這種確認(acknowledge)寫入操作的回應策略/方式,就被稱為 Write Concern 。
用下圖表達可能會更清楚:
上述圖中的的 {w: 2, wtimeout: 500}
是 MongoDB 的 Write Concern 設定,w: 2
旨在告訴 MongoDB 只要 2 個 nodes 有 Apply 寫入操作,就可以進行回應,而 wtimeout: 500
則是超過多少毫秒(milliseconds)就視為 Timeout 避免阻塞寫入操作。
Write Concern 可以使用的設定共有以下 3 種欄位:
{ w: <value>, j: <boolean>, wtimeout: <number> }
w
除了剛剛提到的數字之外,也可以是字串 majority
(例如 { w: "majority" }
), majority
是 MongoDB 5.0 之後的 w
預設值,會由 MongoDB 自動計算 w
的數字,詳細可以查閱文件 Implicit Default Write Concern 。
j
欄位則是代表 MongoDB 在回應的策略/方式上需要等到寫入硬碟的 Journal 才算,可以簡單認為把資料寫回到硬碟才能算,只寫到記憶體內並不算數,是更為嚴格的條件。
wtimeout
是 Timeout 的時間限制,只有在 w
值大於 1 (>1) 才有效。
Read Concern
理解 Write Concern 之後, Read Concern 也是 MongoDB 需要重點理解的部分。
根據 ACID 4 個重要的原則,我們已經知道 Transactions 可以提供 Atomic, 不可分割性的功能,而 Read Concern 其實負責的就是 ACID 中的 Consistency 與 Isolation, 也就是一致性與隔離性。
The readConcern option allows you to control the consistency and isolation properties of the data read from replica sets and replica set shards.
Read Concern 可以做到什麼呢?
這邊舉 2 個例子:
- 提供 Read Your Own Writes 保證(guarantee)
- 解決 Dirty Read
Read Your Own Writes 是 1 個分散式系統的一致性(consistency)保證 ,例如 Primary, Secondary 的資料庫架構,當我們將寫入資料(新增、更新、刪除)到 Primary 之後,再嘗試讀取剛剛寫入的資料,如果我們是從 Secondary 讀取資料,很可能會因為 Secondary node 還沒有同步剛剛寫入的資料,因此找不到剛剛寫入的資料,如下圖:
Read Your Own Writes 就能保證會讀取到剛剛寫入的資料。
再來講講 Dirty Read 。
假設 1 筆資料從 Primary 複製到 Secondary 之前,我們能在 Primary 讀到這筆資料,萬一 Primary 上的資料最後因為 Transaction 失敗或其他緣故,導致資料被 rollback 時,我們就相當於讀到 1 筆幽靈資料(或者稱為 “Dirty Read” ),這種 dirty read 對於資料的一致性來說是種問題,很可能會造成程式不可預期的結果,譬如算錯錢就有可能是 dirty read 所造成的。
針對這些問題,資料庫都會提供多種不同 level 的設定,提供不同程度的保證,而 MongoDB 總共提供 5 種等級的 Read Concern 可以設定:
- local
- available
- majority
- linearizable
- snapshot
上述 5 種 Read Concern level 只有 local
, marjority
與 snapshot
3 種可以用在 multi-document transactions 中。
Read Concern 使用範例如下:
db.collection("users")\
.find({ userId: 'xyz' })\
.readConcern("majority");
以下對 5 種等級的 Read Concern 概要說明,由於 Read Concern 的行為較為複雜,本文無法一一詳述,建議使用前最好能夠詳閱官方文件。
p.s. 不過一般來說應該只要理解 local
, majority
就足以應付大多數情況,詳細可以閱讀 Causal Consistency and Read and Write Concerns 1 文,裡面有整理何種問題應該使用何種 Write Concern 與 Read Concern 設定的表格
p.s. MongoDB 5.0 之後不需要設定就能使用 Read Concern, 早於 5.0 版本的 MongoDB 需要啟用 enableMajorityReadConcern
Read Concern: local
local
是預設的 Read Concern Level 。執行讀取操作的查詢結果,不保證讀取到的資料已經寫入到大多數的 nodes, 也不保證資料不會被 rollback ,換言之,預設會有 dirty read 的可能性。
Read Concern: available
與 local
十分相似,不過它提供最低的 reads 延遲(latency),同樣不保證資料已經寫入到大多數的 nodes, 也不保證資料不會被 rollback, 而且這個選項有讀取到 orphaned documents 的風險。
orphaned documents 是 shared cluster 環境中,一些同時存在於不同 shared 的 documents, 通常 1 個 document 只能存在 1 個 shard, 而造成 1 個 document 同時存在於不同 shard 的可能原因是 data migration 之後的資料清理不完整,或者 data migration 過程的不正常關機。
如果 1 個 collection 沒有使用 shard, 那麼 available
的作用等同於 local
。
Read Concern: majority
majority
確保資料已經被寫入大多數的 nodes, 所以可以確保讀取到的資料不會被 rollback ,但是無法保證資料是最新的。
要使用 majority
選項,必須使用 MongoDB 的 WiredTiger storage engine (MongoDB 3.2 之後預設的 storage engine )。
Read Concern: linearizable
linearizable
跟 majority
有點相似,但是 linearizable
確定可以讀到最新的資料,它甚至可能會等資料寫到多數 nodes 再回傳查詢結果, 而且它只能對 primary node 送出查詢,此外,無法使用 $out
與 $merge
2 個 stage 。
這個設定的查詢成本較高,因為在查詢過程它還需要跟 secondary nodes 做相關確認。
Read Concern: snapshot
從最近一次 snapshot 中讀取資料,此選項只能用於 Multi-document Transactions 與 find, aggregate, distinct 讀取操作。
總結
當談到分散式環境的 MongoDB 的資料一致性(consistency)時, Write Concern 和 Read Concern 是 2 個至關重要的概念。它們分別用於確保寫入操作和讀取操作的可靠性和一致性。
Write Concern 可以控制資料寫入到 MongoDB 的確認(acknowledged)程度。另一方面, Read Concern 則用於控制讀取操作的一致性,例如,使用 majority
的 Read Concern 將確保讀取操作是被大多數節點確認的資料。
總之,如果你使用的是 MongoDB 叢集的話,強烈建議一定要花點時間認識 Read Concern 與 Write Concern!
以上!
Enjoy!
References
What Is Replication In MongoDB?
The difference between “majority” and “linearizable”
Causal Consistency and Read and Write Concerns