正確理解二階段提交(Two-Phase Commit)

明確問題

二階段提交出現的背景是, 當我們使用分佈式系統時, 如果分佈式系統中的機器發生故障之後, 如何保證事務數據的一致性。

從一個場景入手, 假設一個人要從 A 銀行向 B 銀行進行跨行轉賬 100 元。

此時我們需要對 A 銀行數據庫中該用戶的賬戶,做金額扣減操作( - 100), 同時對 B 銀行數據庫中該用戶的賬戶做金額增加操作 ( +100)

這兩個操作( -100 和 + 100) 我們希望它們是一個事務, 要麼同時成功, 要麼同時失敗 。

此時我們的目標是希望有一個

  • 原子性的提交協議 (Atomic Commit Protocol)

草稿方案

  • 場景難點一:
    • A --> B : 你提交我就提交
    • B 不理 A
    • 然後 ?
    • A 和 B 都沒有辦法繼續
      在這裏插入圖片描述

先提出一種拍腦門方案, 設置一個事務協調者( Transaction Coordinator)。

由該協調者分別向 A 銀行 , B銀行發送指令, 發送完畢後直接予以返回成功

在這裏插入圖片描述

顯然, 上述方案會出現很多問題:

  • 問題一: A 銀行餘額不足
    • A 未提交事務, B 卻成功提交
  • 問題二: B 銀行賬戶不存在
    • A 提交事務, B 未提交
  • 問題三: TC 到 B 中間的網絡發生中斷
    • A 提交, B 沒提交
  • 問題四: TC 在給 B 發送指令前宕機
    • A 提交, B 沒提交

原子提交協議希望實現的2個特性

由上面的場景, 我們可以總結出所有原子提交協議希望實現的2個特性

  • 安全性(Safety)
    • 如果任意一方 commit, 所有人必須都 commit
    • 如果任意一方中斷,則沒有任何一個人進行 commit
  • 存活性(Liveness)
    • 沒有宕機或失敗發生時, A 和 B 都能提交, 則提交
    • 如果發生失敗時,最終能達成一個一致性結果(成功/失敗), 予以響應, 不能一直等待

正確的二段提交協議(Two-Phase Commit)

在這裏插入圖片描述

TC 發送 “prepare” 給 A 和 B
等待響應結果
A 和 B 均返回 Yes
A 和 B 任意一個返回 No
TC 向 A 和B 發送 commit 指令, 向 client 發送 ok 響應
TC 向 A 和B 發送 aboort 指令
A 和 B 在收到 TC 發送的 commit 指令後, 執行 commit 操作

上面的流程看似簡單, 但是有一個點容易被忽略:

  • 當 TC 收到 A 和 B 響應 “Yes” 後, 做出了 “commit” 的決定, 向 A /B 發送指令或, 並不需要再等待 A/B 的響應, 可以直接向 client 返回成功
  • 之所以可以這樣做的原因是, 當 A/B 返回 Yes 後, 就代表 A 和 B 都做好了提交準備, 只要 TC 決定要提交,即使 A/B 宕機, 沒有收到 TC 的 Commit 指令, 只要 A/B 被修復重啓, A 和 B 都必須有能力成功完成提交操作。

二階段提交協議如何滿足安全性(Safety)

  • 事務協調者 TC,作爲一箇中心, 統一收集了 A 和 B 是否有意願(有能力)進行 commit
  • 事務協調者 TC 強制保證了, A, B 雙方必須都有意願提交時, 才進行 commit

二階段提交協議如何滿足存活性( Liveness)

遺憾的是: 上面描述的協議無法滿足存活性。

下面分析一下這個協議在執行過程中可能面臨哪些問題

  • 問題1: 響應超時
    • 結點正常運行, 但是沒有正常收到它所期待的響應, 可能原因如下
      • 其他的結點故障了
      • 網絡情況不好, 數據包丟失了或網絡乾脆中斷了
  • 問題2: 重啓
    • 結點宕機, 重啓以後, 如何恢復被中斷的操作

如何應對超時

首先分析整個協議裏面有哪些等待操作?

  1. 事務協調者 TC 需要等待 A 和 B 返回 “yes”/“no” 才能進行下一步操作
    在這裏插入圖片描述
  2. A 和 B 需要等待 TC 發送 “commit”/ “abort” 指令, 才能進行下一步操作
    在這裏插入圖片描述

然後分析等待狀態超時的時候, 是否有辦法繼續協議

  • 事務協調者 TC 需要等待 A 和 B 返回 “yes”/“no” 超時:

    • 此時 , TC 還沒有發送過任何 “commit” 指令
    • TC 此時可以安全地發起終止 “abort” 指令, 放棄 commit
      • 上面這種做法, 保證了安全性, 放棄了存活性
      • 因爲 A, B 可能都做好了準備進行提交, 只是 “yes” 信息沒有成功被 TC 收到, 就導致了整個事務無法提交
      • 這種情況屬於本可以提交而未提交, 也就是說 TC 採取了非常保守的方案
  • A 和 B 需要等待 TC 發送 “commit”/ “abort” 指令 超時:

    • 以 B 爲例進行考慮( A 的情形完全對稱)
    • 如果 B 之前回復的是 “no” , 那此時, B 可以無需等待 TC 回覆就放棄 commit 操作, 因爲 TC 即使收到了響應, 也會回覆 “abort”, 這個行爲是統一的
    • 如果 B 之前回復了 “Yes”,那此時 B 能單方面地直接進行 aboort 操作嗎?
      • 不行! 因爲, TC 此時可能已經成功收到了 A, B 返回的 “Yes”, 並且已經向 A 發送了 “Commit”, 然後再向 B 發送 “commit” 前宕機了
      • 如果 B 放棄了 commit 操作, 就會出現 A 執行了 commit, B 未執行 Commit 的情形, 顯然違背了安全性(Safety)
    • 那 B 能單方面地直接進行 commit 操作嗎?
      • “不行” ! 因爲 A 可能返回給TC 的響應是 “No”
    • 那此時應該怎麼辦?:
      • 方案一: B 一直等待 TC 的 “commit”/ “abort” 指令
      • 方案二(更好): B 針對這種情形發起一輪終止協議操作(Termination Protocol)

超時終止協議

  • B 向 A 發送狀態查詢請求, 詢問 A 是否知道事務已經提交
  • 如果 B沒有收到 A 的響應, B 無法進行後續操作, 只能繼續等待
  • 如果 B 收到了 A 的響應, 則分如下幾種情況:
    • A 回覆說 , 它已經收到了來自 TC 的 “commit”/ “abort” 指令
      • 此時 B 可以執行 “commit”/ “abort”, 應爲 TC 發給 B 的指令肯定和 A 一樣
    • A 回覆說, 它還沒有向 TC 回覆 “yes”/“no”,
      • 此時 B 和 A 都直接執行 abort 操作
      • 不必擔心 TC, 因爲 TC 尚未收到 A 的回覆, 最終會根據 A 和B 的狀態回覆 client
    • A 回覆說, 它向 TC 回覆了 “no”
      • 此時 B 和 A 都直接執行 abort 操作
    • A 回覆說, 它向 TC 回覆了 “yes”
      • 此時 B 不能進行後續操作
      • 因爲 TC 可能已經收到了 A 和 B 的 “Yes” 響應, 並且決定執行 “commit”, 向 A 和 B 發送了“commit” 指令, 只是沒被 A 和 B 收到, 但是 TC 發送 “commit” 之後就會直接向客戶端返回了 “ok”
      • TC 也有可能在等待 A 和 B 的響應過程中超時了, 直接進行了 “abort” 決定, 向 A 和 B 發送了 “abort” 指令, 只是沒被 A 和 B 收到, 但是 TC 發送 “abort” 之後就會直接向客戶端返回了 “fail”

如何應對宕機重啓

基本原則: 一旦 TC 決定了 commit , 那麼任意一個結點都不允許發生回滾

有如下幾種情形需要考慮:

  • TC 在做出決定後, 立即宕機了, 沒有吧 commit 指令成功發送給 A 和 B , 然後被重新啓動
  • A 和/或 B 在發送 Yes 的過程中, 宕機了, 沒有把 " Yes" 成功發送給 TC , 然後被重新啓動

如果所有的結點, 都能知道他們在宕機前的狀態是什麼, 那就有如下幾種解決方案:

  • A 或 B 相互發起之前描述的終止協議 Termination Protocol, 即相互詢問是否知道事務已經交
  • A 和 B 也可以向 TC 發起狀態查詢操作, TC 可能知道事務是否已經提交

如何保證在宕機重啓後, 依舊能夠記得宕機前的狀態:

  • 在發送任何信息給其他結點前, 一定要先行將自己要回復的內容寫入磁盤, 這樣可以保證一旦宕機, 可以知道宕機前的狀態。
    • 對於 TC 而言, 在向 A 和 B 發送 “commit” 指令前, 一定要先行將 “commit” 成功記錄到磁盤
    • 對於 A/B 而言, 在向 TC 發送 “yes” 之前, 一定要先行將 “yes” 記錄成功記錄到磁盤

這樣重啓之後就可以進行如下的操作:

  • 對於TC, 重啓以後, 如果發現磁盤中沒有記錄 “commit” , 那就可以直接進行 “abort” 操作
    • 磁盤中沒有 “commit” , 就說明根本沒向 A/B 發送過 “commit”, 此時是安全的
  • 對於 A 或者 B , 重啓以後, 如果發現磁盤中沒有記錄 “yes” , 那就可以直接進行 “abort” 操作
    • 磁盤中沒有 “yes” , 說明根本沒有向 TC 發送過 “yes”, TC 不可能做出 “commit” 操作, 執行abort 是安全的
  • 對於 A或者 B, 重啓以後, 如果發現磁盤中有 “yes” 記錄, 那就可以發起終止協議 “termination protocol”
    • 終止協議有可能陷入繼續的等待中
  • 如果TC, A, B 都發生了重啓操作, 只要當 3 個結點都恢復以後, 就可以向 TC 發起查詢, 查看TC 的磁盤中是否存在 “commit” 記錄, 如果存在, 則均可進行 “commit” 操作

二階段提交實現的工程化難點

準備階段到底幹了什麼

回顧之前的內容,可以發現, 二階段提交協議(Two-Phase Commit)的核心是

  • 引入了一個事務協調者(TC)
  • 在真正的提交操作前, 增加了一個準備階段, 收集業務結點是否有能力進行提交

部分程序員可能會對其工程實現的一個關鍵點產生誤解:

  • 準備階段就是開啓一個事務, 執行所有的業務代碼, 都不報錯, 不執行事務的 commit 操作, 然後向 TC 回覆 “Yes”, 表示我已準備好提交

這種做法並不滿足二階段提交協議對於準備操作的要求:

  • 二階段提交協議中, 業務結點回復 “Yes” , 代表它做好了提交操作的所有準備, 只要結點回復了 “Yes”, 即使突然發生宕機, 只要結點重新啓動, 收到了 TC 發送的 commit 指令, 必須依舊能正確提交
  • 普通數據庫如果在在一個事務中間發生了宕機(比如數據庫所在機器直接停電), 重啓以後, 數據庫的默認行爲是對處於中間狀態的事務進行回滾操作, 並不具備繼續等待並接受 commit 指令的能力

以之前提到的轉賬操作爲例, 如果 A 銀行需要對轉賬客戶的賬戶執行 -100 元操作, 當它向 TC 回覆了 “Yes” 前, 應該完成以下操作:

  • 確定賬戶上有 100 待扣減, 將這100 元凍結, 其他的操作無法解凍, 轉移這100元。
  • 留下必要的持久化記錄, 確保即使宕機重啓, 收到 “abort” 指令也有能力回滾到100 元被凍結前的狀態
  • 留下必要的持久化記錄, 確保即使宕機重啓, 收到 “commit” 指令也有能力正確提交, 完成 -100 元操作
  • 留下必要的持久化記錄, 標識自己已經完成了準備階段的所有操作, 要向 TC 回覆 “Yes” 指令

只有以上這些操作都成功完成以後, 銀行 A 才能嘗試向 TC 發送 “Yes” 指令,否則就有違二階段提交協議

事務協調者是第三方嗎

二階段提交中, 除了準備階段, 另一個顯眼的角色就是事務協調者( Transaction Coordinator)。

此時就會有部分同學產生困惑, 事務協調者是否一定得是一個獨立的機器,處於獨立的結點。

答案: 並不是!

從準備階段的要求就可以看出。 二階段提交協議的核心, 是描述了在每一步操作前, 每一種角色應該達到什麼狀態, 具備什麼能力。 具體在工程實現中, 這幾種角色分佈在幾個結點, 以什麼方式去實現, 都是可以的。

以前文所舉的銀行轉賬操作爲例。

客戶端 client 發起一個轉賬操作請求給 TC, 這個 TC 完全可以就屬於銀行 A。 只要銀行 A 實現的 TC 在銀行 A 數據庫發起 -100 元的操作前,依舊先按照協議要求;

  • 模擬 prepare 階段, 進行持久化記錄
  • 做好 -100 元隨時提交或回退的準備

即可以根據銀行 B 的準備階段應答結果, 進行後續的操作。

二階段提交協議(Two-Phase Commit)的總結

二階段提交所說的二階段分別指: Prepare , Commit 兩個階段

二階段提交滿足如下性質:

  • 安全性: 所有的結點最終會達成一致的決定
  • 安全性: 只有當所有人都說了 “yes”, 纔會執行提交
  • 存活性: 如果沒有宕機和信息丟失, 提交肯定可以完成
  • 存活性: 如果發生宕機和信息丟失, 只要最終修復, 並且重啓, 等待足夠長的時間, 最終一定可以達成某項一致的結果(abort 或者 commit)

可能有同學會有疑問, 那如果宕機不能被修復, 那些處於中間狀態的記錄怎麼辦:

1985 年 Fischer, Lynch, Paterson 提出了定理:

no distributed asynchronous protocol can correctly agree in presence of crash-failures

翻譯一下就是

在出現宕機時(最終沒有修復並重啓), 不存在一種分佈式的異步協議可以正確地達成一致結果(同時提供安全性和存活性)。

所以有故障發生, 不修復還想保證記錄達成一致? 洗洗睡吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章