分散式系統(三)——分散式事務

妙妙園發表於2022-01-01

事務:使用者的每個命令操作作為一個整體,不會因為失敗而被分割,也不會被其他活動看到中間狀態。事務處理系統要求程式設計師對這些讀、寫操作標明起始和結束,這樣才能知道事務的起始和結束。事務處理系統保證在事務的開始和結束之間的行為是可預期的。
分散式事務的考量出現在這樣的場景下:資料被分片儲存到許多不同的伺服器上。一個操作可能要求從多個伺服器上修改或者讀取資料,分散式系統要嘗試向使用者隱藏將資料分割在多個伺服器上帶來的複雜度。
資料庫對於正確性有一個概念稱為ACID:

  • Atomic:原子性。在一個事務裡,要麼所有的寫資料全都完成,要麼所有的寫資料全都沒有完成。
  • Consistent:一致性。
  • Isolate:隔離性。事務不能看到彼此的中間狀態,只能看到完成的或者未完成的事務結果。通常來說,隔離性意味著可序列化。
  • Durable:持久化。事務提交後,資料庫向使用者反饋事務執行完畢後,資料庫中的修改是持久的,不會因為一些錯誤而被擦除。

事務可能會在執行過程中Abort(例如除0錯誤、訪問一個不存在的記錄、死鎖等),此時事務可能已經修改了資料庫中的部分記錄,我們需要能夠回退這些事務,撤回已進行的修改。
分散式事務由兩部分組成:併發控制(concurrency control)和原子提交(atomic commit)。

併發控制

在併發控制中,主要有兩種策略:悲觀併發控制和樂觀併發控制。使用哪種策略取決於環境中衝突發生的頻率。
悲觀併發控制:資料庫使用鎖來保護資料,事務在使用任何鎖之前,需要獲得資料的鎖,如果一些其他的事務已經在使用這裡的資料,鎖會被它們持有。在悲觀系統中,如果有鎖衝突,就會造成延時等待,為正確性而犧牲效能。
樂觀併發控制:不用擔心其他事務是否正在讀寫你要使用的資料,而是自顧自地繼續執行。通常來說這些執行會有一些臨時區域,只有在事務最後的時候才會檢查是不是有其他事務干擾了你。這樣做的優點是不用承擔鎖帶來的效能損耗,缺點是如果有其他事務在同一時間修改了你關心的資料並造成衝突,那麼必須Abort當前事務並重試。
悲觀併發控制涉及的基本就是鎖機制。這裡的鎖是兩階段鎖(Two-Phase Locking)。

兩階段鎖

當事務需要使用一些資料記錄的時候,

  1. 第一階段獲取鎖(expanding phase):在使用任何資料之前,在執行任何資料的讀寫之前,先獲取鎖;
  2. 第二階段持有鎖(constracting phase):事務必須持有任何已經獲得的鎖,知道事務提交或者abort,不允許在事務的中間過程釋放鎖,只能不斷累積持有的鎖,直到事務完成。
    兩階段鎖的正確性證明:https://zhuanlan.zhihu.com/p/59535337

兩階段提交

資料庫的原子性是指事務的每一個部分都執行,或者任何一個部分都不執行。常用來達到原子性的方法是使用原子提交協議(Atomic Commit Protocols)。對於分散式系統,通常來說,原子提交協議的工作是幫助計算機決定是否能執行這個操作,是否執行了某個操作,或者出錯了的時候所有計算機都要獲得訊息並且不再執行(回退)自己的操作。兩階段提交是一種原子提交協議,不僅被分散式資料庫使用,也被各種分散式系統使用。

  1. 第一階段投票階段(prepare phase):事務協調者節點給每個參與者節點傳送prepare訊息,每個參與者要麼直接返回失敗,要麼在本地執行事務,寫本地的redo和undo日誌,但不提交。
  • 協調者節點向所有的參與者節點傳送待執行的操作,詢問是否可以執行提交操作,並等待響應。
  • 參與者節點檢查事務許可權,寫入undo和redo日誌,再執行事務操作。
  • 參與者節點響應協調者節點的詢問,如果執行成功,就返回同意,不成功就abort。
  1. 第二階段提交階段(commit phase):
  • 如果協調者收到參與者的abort訊息或者超時,那麼直接給每個參與者傳送回滾訊息;如果所有參與者都同意提交,傳送commit訊息給所有參與者。在傳送abort/commit訊息之前,持久化事務。
  • 參與者根據協調者指令執行回滾或提交操作,最後再釋放事務處理過程中所有的鎖。並反饋給協調者。
    tips:兩階段提交的升級版是三階段提交,待補充。

故障恢復

【在各種故障場景下考慮兩階段提交協議對原子性的保障】
對於協調者:

  1. 傳送commit訊息之前崩潰:沒有一個參與者會commit事務,可以直接abort事務。
  2. 傳送完一個或者多個commit訊息之後崩潰:協調者不可以忘記相關的事務,因為可能已經有參與者將事務在本地commit了,為此,協調者在傳送commit訊息之前,必須將事務的資訊寫入到自己的log,並持久化儲存到磁碟中。

對於參與者:

  1. 參與者回覆prepare之前故障:協調者abort事務。
  2. 參與者回覆prepare之後,收到commit/abort之前故障:故障導致記憶體中有關事務、鎖等的資訊全部丟失,但由於是先寫日誌再執行操作,日誌可以持久化在磁碟上持久化儲存,故障重啟就可以根據日誌和協調者的commit/abort訊息恢復資訊。(tips:由於參與者必須將日誌寫道磁碟後才能執行以及回覆協調者的prepare資訊,因此兩階段提交協議的速度並不快。)
  3. 參與者處理完commit訊息之後崩潰:事務已經執行完畢,不會出現錯誤。

兩階段提交的效率很低,因為它要求事務涉及的所有參與者都能正常工作。因此,兩階段提交雖然具備故障下的正確性,但不具備可用性。可以結合raft協議,對每個參與者節點構建一個raft叢集,結合兩階段提交和raft協議的思想來同時獲得高可用和原子提交。

相關文章