2018年4月25日

[筆記] MongoDB: Two Phase Commit - Multi-Document Transactions

這篇的內容主要是來自 MongoDB 官方網站上的 Manual 中的這篇。簡單總結:two phase commit 是為了模擬 Transaction 的特性。

基本上 MongoDB 有保證對單一 document (註1) 讀寫時有 atomic 的特性,但是為了效能考量,如果變更的內容有包含數個 document 就不保證 atomic。所以像是 manual 上的那篇的例子:從 A 的帳號轉帳到 B 的帳號這種事情因為牽涉到 2 個 document,因此在多執行緒的環境下不能確保資料不會出狀況。Two phase commit 就是為了這種情形用來讓 multi-document 的更新也具有 transaction 的效果

Step 1: 建立交易資訊

其實概念說穿了很簡單:既然有保證單一 document 的操作有 atomic,那就先新增一個 document 記錄這一筆交易資訊有哪些操作要處理。以轉帳這個例子來說就會包含:A (轉出帳號),B (轉入帳號),金額,狀態。
db.transactions.insert(
    { _id: 1, source: "A", destination: "B", value: 100, state: "initial" }
)
狀態那項就是用來查詢目前這筆交易的處理狀態,分成 initial (未處理) --> pending (有人抓去看了,處理中) --> applied (處理完畢,要開始收尾了) --> done (處理完畢)。因為單一 document 具有 atomic 特性,對這個交易資訊的讀寫可以確保不會有人干擾 & 絕對是最新結果,因此我們就可以利用這個 document 去了解目前這筆交易處理的進度如何。

舉例來說,我們可以利用 {state:"initial"} 查詢條件去找出尚未被處理的交易資訊,這個時候我們不用擔心找出來的交易資訊有人正在把他的 state 從 "initial" 改成 "pending"
var t = db.transactions.findOne( { state: "initial" } )

Step 2: 查詢未處理的交易資訊並更新狀態

當我們找出一筆符合條件的交易資訊時,接下來就可以把這筆交易資訊的狀態從 "initial" 改成 "pending"
db.transactions.update(
    { _id: t._id, state: "initial" },
    { $set: { state: "pending" }   }
)
這邊要注意的是 update 的更新條件一樣要把 state 列進去,因為在執行 update 前或執行中時可能已經有人正在修改 tstate

Step 3: 對帳號做更新

交易資訊的狀態更新後接下來當然就是去對 A 跟 B 的帳號做更新囉:
db.accounts.update(
   { _id: t.source, pendingTransactions: { $ne: t._id } },
   { $inc: { value: -t.value }, $push: { pendingTransactions: t._id } }
)
db.accounts.update(
   { _id: t.destination, pendingTransactions: { $ne: t._id } },
   { $inc: { value: t.value }, $push: { pendingTransactions: t._id } }
)
同樣的,為了避免有多餘一個執行緒正在處理這筆交易資訊,在 A 跟 B 的 document 中都有一個 key 叫 pendingTransactions 用來記錄有哪些交易資訊正在對這個帳號做更新。所以後做的執行緒一旦發現 pendingTransactions 中已經有 t_id 了就不會處理,這樣就能避免重複處理。

Step 4: 更新交易資訊為已處理

當 A 跟 B 的資訊都處理完畢了我們就可以把交易資訊的狀態變更為 "applied"
db.transactions.update(
   { _id: t._id, state: "pending" },
   { $set: { state: "applied" }   }
)

Step 5: 收尾,把這筆交易資訊移除掉

因為帳號中還有目前這筆交易資訊的資料,所以要把它移除掉:
db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)

Step 6: 最後就能標記這筆交易資訊已經完成

db.transactions.update(
   { _id: t._id, state: "applied" },
   { $set: { state: "done" }      }
)

以上,大功告成。

不過真要做的話不靠 two phase commit 要達到同樣效果應該也是辦得到的,只是會這樣會讓資料變得更加複雜,又效能上來說不見得會比這種方法好,再考慮維護性的話,two phase commit 確實會是個不錯的方法。

--
註解:
  1. MongoDB 的階層簡單來說是這樣:
    DB --> Collection --> Document
    一個 DB 可以包含多個 collection,一個 collection 包含多個 document,我們存取的資料就是指 document。可以把 MDB 的 document 當成 RDBMS 中的一個 row,RDBMS 中的 table 對應過來就是 MongoDB 的 Collection。

沒有留言:

張貼留言