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 。

用下圖表達可能會更清楚:

write-concern.png

上述圖中的的 {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 個例子:

  1. 提供 Read Your Own Writes 保證(guarantee)
  2. 解決 Dirty Read

Read Your Own Writes 是 1 個分散式系統的一致性(consistency)保證 ,例如 Primary, Secondary 的資料庫架構,當我們將寫入資料(新增、更新、刪除)到 Primary 之後,再嘗試讀取剛剛寫入的資料,如果我們是從 Secondary 讀取資料,很可能會因為 Secondary node 還沒有同步剛剛寫入的資料,因此找不到剛剛寫入的資料,如下圖:

read_your_write.png

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 可以設定:

  1. local
  2. available
  3. majority
  4. linearizable
  5. snapshot

上述 5 種 Read Concern level 只有 local, marjoritysnapshot 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

linearizablemajority 有點相似,但是 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

MongoDB - Transactions

What Is Replication In MongoDB?

MongoDB - Read Concern

The difference between “majority” and “linearizable”

Causal Consistency and Read and Write Concerns

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!