在多數商業應用下,每一個請求都可能需要對資料庫進行操作,在過去 Monolithic 的架構下,如果只針對單一資料庫做操作,在 事務(Transaction) 管理相對簡單,當系統規模越來越龐大時,可能需要處理多個資料庫之間的 Transaction 管理。在每個服務都擁有私有資料庫的微服務架構下,跨越多個服務執行一個 Transaction 成為一項巨大挑戰。
舉個例子,假如有一個購物系統,共有三個服務:
下方是建立訂單的流程圖,當 Client 向訂單服務發送建立訂單的請求時,訂單服務會通知商品服務減少可銷售的庫存,並將該訂單的狀態改為「準備中」,接著,通知物流服務建立物流單,當物流服務建立好物流單之後,訂單服務會更新該訂單的物流資訊:
假如在建立訂單的流程中,訂單成功建立了、可銷售庫存順利減少了,但物流單建立失敗了,此時對於訂單服務與商品服務來說,它們完成了自身的 Transaction,不過就以整個流程而言,資料確實產生了 不一致 的情況,這將導致使用者感到困惑,如果不處理這個問題,最終會失去客戶的信賴。
從上方範例可以得知,這種跨服務的 資料一致性 是非常重要的,但也是相當困難的,在過去會使用 分散式事務(Distributed Transaction) 的方式來處理,二階段提交(Two Phase Commit) 就是其中一種處理手段,可以用來確保所有參與者同時完成 提交(Commit) 或是同時針對失敗進行 回復(Rollback)。
什麼是 Two Phase Commit 呢?簡單來說,會有一個 協調者(Coordinator) 負責處理 Transaction,它會分兩階段進行:
不過 Distributed Transaction 帶來的問題很明顯,就是它會是以 同步 的方式來確保資料的一致性,雖然可以確保多個參與方之間的資料一致性,但對微服務架構而言,將會因此降低可用性,因為會花費許多資源在處理這些操作。以現在的環境來說,會更傾向使用以 非同步 為基礎的 Saga Pattern。
Saga 是一種基於非同步處理資料一致性的機制。它捨棄了Distributed Transaction 的強一致性,改為依賴每個服務的 Transaction 生命週期來觸發非同步事件,進而執行下一個服務的 Transaction。透過一連串的非同步事件,來完成整個流程中各個服務之間的 Transaction,從而保證 最終一致性。
以上方建立訂單的情境來說,以 Saga 的方式來處理就會依照下圖的順序來執行對應的 Transaction:
由於 Saga 是透過非同步的方式處理各個服務間的 Transaction,它必須克服與面對下列幾個問題:
由於 Saga 是一連串獨立的 Transaction 操作,當過程中有服務發生錯誤,前面已經完成的服務是無法 Abort 的,所以在實作 Saga 時,需要識別哪些 Transaction 會涉及資料的改變,並針對這些 Transaction 去設計 Compensation Transaction,當執行該 Compensation Transaction 時,服務必須對對應的 Transaction 進行一定程度的校正,比如:本來建立好的訂單,因為商品服務故障導致可銷售庫存沒有減少,這時訂單建立的事實已經發生,所以應該用拒絕訂單的方式進行 沖銷:
我們來更進一步來探討 Compensation Transaction 的運作,假如正在執行一個 Saga 的流程,執行順序為 T1 到 Tn,當執行到第 n + 1
個 Transaction 時發生錯誤,此時應該要針對前面 n
個 Transaction 進行補償,Saga 的流程必須以 相反的順序 開始執行 Compensation Transaction,也就是 Cn 到 C1,如下圖所示:
由於 Saga 並不滿足 隔離性(Isolation),這表示在 Saga 執行的過程中,有可能遇到以下問題:
為了解決上述問題,需要使用一些 Countermeasure 的技巧來防止問題產生,但在講解 Countermeasure 技巧之前,需要先了解 Saga 流程中各個服務的 Transaction 類型,這對於 Countermeasure 會很有幫助:
以上方建立訂單的流程來說,可以識別出各個 Transaction 的類型:
了解三種類型的 Transaction 後,就可以來了解幾個常見的 Countermeasure。
Saga 流程中的 Compensatable Transaction 會在建立或更新資料時以某種標誌來鎖定資源,目的是防止其他 Transaction 讀取該資料或提醒該 Transaction 要更加謹慎處理。通常這個標誌會由 Retriable Transaction 或 Compensation Transaction 修改、清除:
以上方建立訂單的範例來說,可以在訂單中加上 _lock
欄位:
_lock
的值為 APPROVAL_PENDING
。_lock
的值改為 APPROVED
。_lock
的值改為 REJECTED
。不過語意鎖本身只是靠特定字串來標誌狀態,還是需要針對這些標誌的特徵值來做出防備。以上方範例來說,在 _lock
為 APPROVAL_PENDING
時,如果遇到「取消訂單」的 Saga 時,應該要拒絕或阻塞該操作。
將更新的操作設計成不計較執行順序的,也就是每個操作都是可交換的,這樣的方式可以避免 Lost Updates。
以上方建立訂單的範例來說,會針對可銷售庫存做增減,一般狀況下會以「庫存減 1」的方式減少可銷售庫存;反之,當更新訂單狀態失敗時,會以「庫存加 1」的方式補回可銷售庫存。
以最悲觀的角度來考慮 Saga 執行的順序,來降低 Dirty Reads 帶來的風險,不過須特別注意,它的預防並非 100%。
讀取資料時,可能會拿到過時的值,所以 Reread Value 的做法相當簡單暴力,在執行更新之前,再次讀取資料確保資料有沒有異動過,如果沒有異動就執行更新;反之,要決定是否中止操作。舉例來說,在付款流程中,如果我們讀取到的訂單狀態為「待付款」,但後續操作該訂單狀態變更為「已取消」,此時需要重新讀取訂單狀態,確認其當前狀態是否允許進行扣款操作。
在 Monolithic 架構下,管理 Transaction 是相對容易的,但在微服務架構下,要實現跨服務的 Transaction 是相當複雜的。在過去,會使用 Two Phase Commit 的技巧來保證資料的強一致性,但這種同步機制效能不佳,在可用性方面表現較差,在需要高效運作的微服務架構下並不一定是好的選擇。為了解決資料一致性與效能問題,而產生了基於非同步的 Saga Pattern,它透過一連串非同步事件將各個獨立的 Transaction 串起來,雖然犧牲了強一致性,但依然可以保證資料最終一致,且提升了系統可用性。由於 Saga 是多個 Transaction 所組合起來的流程,勢必要針對中途失敗的情況做處理,所以需要為 Compensatable Transactions 設計對應的 Compensation Transaction 來校正資料,另外,Saga 還需克服缺少 Isolation 所帶來的問題,包含:Dirty Reads、Lost Updates,所以需要使用 Countermeasure 來避免異常發生。
由於 Saga 是一個較複雜的議題,本篇只針對其概念作說明,下一篇將會介紹要如何實作 Saga,敬請期待!