資料庫分散式事務的實現原理!

趙鈺瑩發表於2018-08-23

事務是資料庫系統中非常有趣也非常重要的概念,它是資料庫管理系統執行過程中的一個邏輯單元,它能夠保證一個事務中的所有操作要麼全部執行,要麼全不執行;在 SOA 與微服務架構大行其道的今天,在分散式的多個服務中保證業務的一致性就需要我們實現分散式事務。

在這篇文章中,我們將介紹 、分散式事務的理論基礎以及實現原理。

事務

在文章的開頭,我們已經說過事務是資料庫管理系統執行過程中的一個邏輯單位,它能保證一組資料庫操作要麼全部執行,要麼全不執行,我們能夠透過事務將資料庫從一個狀態遷移到另一個狀態,在每一個狀態中,資料庫中的資料都保持一致性。

資料庫事務擁有四個特性,原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability):

我們經常將這上述的四大特性簡寫為 ACID,而資料庫事務的實現原理其實也就是實現這四大特性的原理。

實現原理

在之前的文章   中其實已經對如何實現事務的 ACID 這幾個基本屬性給出了比較詳細的介紹和分析,在這裡就簡單介紹幾個比較重要的實現細節,關於展開的內容,可以閱讀上述文章。

事務日誌

為了實現確保事務能在執行的任意過程中回滾(原子性)並且提交的事務會永久儲存在資料庫中,我們會使用事務日誌來儲存事務執行過程中的資料庫的變動,每一條事務日誌中都包含事務的 ID、當前被修改的元素、變動前以及變動後的值。

當我們有以上的事務日誌之後,一旦需要對事務進行回滾就非常容易了,資料庫會根據上述日誌生成一個相反的操作恢復事務發生之前的狀態;事務日誌除了能夠對事務進行回滾保證原子性之外,還能夠實現永續性,當一個事務常食對資料庫進行修改時,它其實會先生成一條日誌並重新整理到磁碟上,寫日誌的操作由於是追加的所以非常快,在這之後才會向資料庫中寫入或者更新對應的記錄。

在 MySQL 最常見的儲存引擎 InnoDB 中,事務日誌其實有兩種,一種是回滾日誌(undo log),另一種是重做日誌(redo log),其中前者保證事務的原子性,後者保證事務的永續性,兩者可以統稱為事務日誌。

併發控制

資料庫作為最關鍵的後端服務,很難想象只能序列執行每一個資料庫操作帶來的效能影響,然而在併發執行 SQL 的過程中就可能無法保證資料庫對於隔離性的要求,歸根結底這就是一致性、隔離性與效能之間的權衡。

為了避免併發帶來的一致性問題、滿足資料庫對於隔離性要求,資料庫系統往往都會使用併發控制機制儘可能地充分利用機器的效率,最常見的幾種併發控制機制就是鎖、時間戳和 MVCC:

作為悲觀併發控制機制,鎖使用在更新資源之前對資源進行鎖定的方式保證多個資料庫的會話同時修改某一行記錄時不會出現脫離預期的行為,而時間戳這種方式在每次提交時對資源是否被改變進行檢查。

淺談資料庫併發控制 - 鎖和 MVCC  中,作者介紹過幾種不同的併發控制機制的實現原理,想要了解更多相關的內容的可以閱讀這篇文章。

分散式事務

從廣義上來看,分散式事務其實也是事務,只是由於業務上的定義以及微服務架構設計的問題,所以需要在多個服務之間保證業務的事務性,也就是 ACID 四個特性;從單機的資料庫事務變成分散式事務時,原有單機中相對可靠的方法呼叫以及程式間通訊方式已經沒有辦法使用,同時由於網路通訊經常是不穩定的,所以服務之間資訊的傳遞會出現障礙。

模組(或服務)之間通訊方式的改變是造成分散式事務複雜的最主要原因,在同一個事務之間的執行多段程式碼會因為網路的不穩定造成各種奇怪的問題,當我們透過網路請求其他服務的介面時,往往會得到三種結果:正確、失敗和超時,無論是成功還是失敗,我們都能得到唯一確定的結果,超時代表請求的發起者不能確定接受者是否成功處理了請求,這也是造成諸多問題的誘因。

系統之間的通訊可靠性從單一系統中的可靠變成了微服務架構之間的不可靠,分散式事務其實就是在不可靠的通訊下實現事務的特性。無論是事務還是分散式事務實現原子性都無法避免對持久儲存的依賴,事務使用磁碟上的日誌記錄執行的過程以及上下文,這樣無論是需要回滾還是補償都可以透過日誌追溯,而分散式事務也會依賴資料庫、Zookeeper 或者 ETCD 等服務追蹤事務的執行過程,總而言之,各種形式的日誌是保證事務幾大特性的重要手段。

2PC 與 3PC

兩階段提交是一種使分散式系統中所有節點在進行事務提交時保持一致性而設計的一種協議;在一個分散式系統中,所有的節點雖然都可以知道自己執行操作後的狀態,但是無法知道其他節點執行操作的狀態,在一個事務跨越多個系統時,就需要引入一個作為協調者的元件來統一掌控全部的節點並指示這些節點是否把操作結果進行真正的提交,想要在分散式系統中實現一致性的其他協議都是在兩階段提交的基礎上做的改進。

兩階段提交的執行過程就跟它的名字一樣分為兩個階段,投票階段和提交階段,在投票階段中,協調者(Coordinator)會向事務的參與者(Cohort)詢問是否可以執行操作的請求,並等待其他參與者的響應,參與者會執行相對應的事務操作並記錄重做和回滾日誌,所有執行成功的參與者會向協調者傳送 AGREEMENT 或者 ABORT 表示執行操作的結果。

當所有的參與者都返回了確定的結果(同意或者終止)時,兩階段提交就進入了提交階段,協調者會根據投票階段的返回情況向所有的參與者傳送提交或者回滾的指令。

當事務的所有參與者都決定提交事務時,協調者會向參與者傳送 COMMIT 請求,參與者在完成操作並釋放資源之後向協調者返回完成訊息,協調者在收到所有參與者的完成訊息時會結束整個事務;與之相反,當有參與者決定 ABORT 當前事務時,協調者會向事務的參與者傳送回滾請求,參與者會根據之前執行操作時的回滾日誌對操作進行回滾並向協調者傳送完成的訊息,在提交階段,無論當前事務被提交還是回滾,所有的資源都會被釋放並且事務也一定會結束。

兩階段提交協議是一個阻塞協議,也就是說在兩階段提交的執行過程中,除此之外,如果事務的執行過程中協調者永久當機,事務的一部分參與者將永遠無法完成事務,它們會等待協調者傳送 COMMIT 或者 ROLLBACK 訊息,甚至會出現多個參與者狀態不一致的問題。

3PC

為了解決兩階段提交在協議的一些問題,三階段提交引入了超時機制和準備階段,如果協調者或者參與者在規定的之間內沒有接受到來自其他節點的響應,就會根據當前的狀態選擇提交或者終止整個事務,準備階段的引入其實讓事務的參與者有了除回滾之外的其他選擇。

當參與者向協調者傳送 ACK 後,如果長時間沒有得到協調者的響應,在預設情況下,參與者會自動將超時的事務進行提交,不會像兩階段提交中被阻塞住;上述的圖片非常清楚地說明了在不同階段,協調者或者參與者的超時會造成什麼樣的行為。

XA 事務

MySQL 的 InnoDB 引擎其實能夠支援分散式事務,也就是我們經常說的 XA 事務;XA 事務就是用了我們在上一節中提到的兩階段提交協議實現分散式事務,其中事務管理器為協調者,而資源管理器就是分散式事務的參與者。

到這裡,其實我們已經能夠清晰地知道 MySQL 中的 XA 事務是如何實現的:

  • 資源管理器提供了訪問事務資源的能力,資料庫就是一種常見的資源管理器,它能夠提交或者回滾其管理的事務;

  • 事務管理器協調整個分散式事務的各個部分,它與多個資源管理器通訊,分別處理他們管理的事務,這些事務都是整體事務的一個分支。

正如兩階段提交協議中定義的,MySQL 提供的 XA 介面可以非常方便地實現協議中的投票和提交階段,我們可以透過一下的流程圖簡單理解一下 MySQL XA 的介面是如何使用的:

XA 確實能夠保證較強的一致性,但是在 MySQL XA 的執行過程中會對相應的資源加鎖,阻塞其他事務對該資源的訪問,如果事務長時間沒有 COMMIT 或者 ROLLBACK,其實會對資料庫造成比較嚴重的影響。

Saga

兩階段提交其實可以保證事務的強一致性,但是在很多業務場景下,我們其實只需要保證業務的最終一致性,在一定的時間視窗內,多個系統中的資料不一致是可以接受的,在過了時間視窗之後,所有系統都會返回一致的結果。

Saga 其實就一種簡化的分散式事務解決方案,它將一系列的分散式操作轉化成了一系列的本地事務,在每一個本地事務中我們都會更新資料庫並且向叢集中的其他服務傳送一條的新的訊息來觸發下一個本地的事務;一旦本地的事務因為違反了業務邏輯而失敗,那麼就會立刻觸發一系列的回滾操作來撤回之前本地事務造成的副作用。

LLT

相比於本地的資料庫事務來說,長事務(Long Lived Transaction)會對一些資料庫資源持有相對較長的一段時間,這會嚴重地影響其他正常資料庫事務的執行,為了解決這一問題,Hector Garcia-Molina 和 Kenneth Salem 在 1987 釋出了論文   用於解決這一問題。

如果一個 LLT 能夠被改寫成一系列的相互交錯重疊的多個資料庫事務,那麼這個 LLT 就是一個 Saga;資料庫系統能夠保證 Saga 中一系列的事務要麼全部成功執行、要麼它們的補償事務能夠回滾全部的副作用,保證整個分散式事務的最終一致性。Saga 的概念和它的實現都是非常簡單的,但是它卻能夠有很大的潛力增加整個系統的處理能力。

事務越長並且越複雜,那麼這個事務由於異常而被回滾以及死鎖的可能性就會逐漸增加,Saga 會將一個 LLT 分解成多個短事務,能夠非常明顯地降低事務被回滾的風險。

協同與編排

當我們使用 Saga 模式開發分散式事務時,有兩種協調不同服務的方式,一種是協同(Choreography),另一種是編排(Orchestration):

如果對於一個分散式事務,我們採用協同的方式進行開發,每一個本地的事務都會觸發一個其他服務中的本地事務的執行,也就是說事務的執行過程是一個流的形式進行的:

當我們選擇使用協同的方式處理事務時,服務之間的通訊其實就是透過事件進行的,每一個本的事務最終都會向服務的下游傳送一個新的事件,既可以是訊息佇列中的訊息,也可以是 RPC 的請求,只是下游提供的介面需要保證冪等和重入。

除此之外,透過協同方式建立的分散式事務其實並沒有明顯的中心化節點,多個服務參與者之間的互動協議要從全域性來定義,每個服務能夠處理以及傳送的事件和介面都需要進行比較嚴謹的設計,儘可能提供抽象程度高的事件或者介面,這樣各個服務才能實現自治並重用已有的程式碼和邏輯。

如果我們不想使用協同的方式對分散式事務進行處理,那麼也可以選擇編排的方式實現分散式事務,編排的方式引入了中心化的協調器節點,我們透過一個 Saga 物件來追蹤所有的子任務的呼叫情況,根據任務的呼叫情況決定是否需要呼叫對應的補償方案,並在網路請求出現超時時進行重試:

在這裡我們就引入了一箇中心化的『協調器』,它會儲存當前分散式事務進行到底的狀態,並根據情況對事務進行回滾或者提交操作,在服務編排的過程中,我們是從協調者本身觸發考慮整個事務的執行過程的,相對於協同的方式,編排實現的過程相對來說更為簡單。

協同與編排其實是兩種思路截然相反的模式,前者強調各個服務的自治與去中心化,後者需要一箇中心化的元件對事務執行的過程進行統一的管理,兩者的優缺點其實就是中心化與去中心化的優缺點,中心化的方案往往都會造就一個『上帝服務』,其中包含了非常多組織與整合其他節點的工作,也會有單點故障的問題,而去中心化的方案就會帶來管理以及除錯上的不便,當我們需要追蹤一個業務的執行過程時就需要跨越多個服務進行,增加了維護的成本。

下游約束

當我們選擇使用 Saga 對分散式事務進行開發時,會對分散式事務的參與者有一定的約束,每一個事務的參與者都需要保證:

  1. 提供介面和補償副作用的介面;

  2. 介面支援重入並透過全域性唯一的 ID 保證冪等;

這樣我們就能夠保證一個長事務能夠在網路通訊發生超時時進行重試,同時在需要對事務進行回滾時呼叫回滾介面達到我們的目的。

小結

Saga 這種模式其實完全放棄了同時滿足事務四大基本特性 ACID 的想法,而是選擇降低實現分散式事務的難度並減少資源同步以及鎖定帶來的問題,選擇實現 BASE(Basic Availability, Soft, Eventual consistency) 事務,達到業務上的基本可用以及最終一致性,在絕大多數的業務場景中,實現最終一致性就能夠基本滿足業務的全部需求,極端場景下還是應該選擇兩階段提交或者乾脆放棄分散式事務這種易錯的實現方式,轉而使用單機中的資料庫事務來解決。

訊息服務

分散式事務帶來複雜度的原因其實就是由於各個模組之間的通訊不穩定,當我們發出一個網路請求時,可能的返回結果是成功、失敗或者超時。

網路無論是返回成功還是失敗其實都是一個確定的結果,當網路請求超時的時候其實非常不好處理,在這時呼叫方並不能確定這一次請求是否送達而且不會知道請求的結果,但是訊息服務可以保證某條資訊一定會送達到呼叫方;大多數訊息服務都會提供兩種不同的 QoS,也就是服務的等級。

最常見的兩種服務等級就是 At-Most-Once 和 At-Least-Once,前者能夠保證傳送方不對接收方是否能收到訊息作保證,訊息要麼會被投遞一次,要麼不會被投遞,這其實跟一次普通的網路請求沒有太多的區別;At-Least-Once 能夠解決訊息投遞失敗的問題,它要求傳送者檢查投遞的結果,並在失敗或者超時時重新對訊息進行投遞,傳送者會持續對訊息進行推送,直到接受者確認訊息已經被收到,相比於 At-Most-Once,At-Least-Once 因為能夠確保訊息的投遞會被更多人使用。

除了這兩種常見的服務等級之外,還有另一種服務等級,也就是 Exactly-Once,這種服務等級不僅對傳送者提出了要求,還對消費者提出了要求,它需要接受者對接收到的所有訊息進行去重,傳送者和接受者一方對訊息進行重試,另一方對訊息進行去重,兩者分別部署在不同的節點上,這樣對於各個節點上的服務來說,它們之間的通訊就是 Exactly-Once 的,但是需要注意的是,Exacly-Once 一定需要接收方的參與。

我們可以透過實現 AMQP 協議的訊息佇列來實現分散式事務,在協議的標準中定義了 tx_select、tx_commit 和 tx_rollback 三個事務相關的介面,其中 tx_select 能夠開啟事務,tx_commit 和 tx_rollback 分別能夠提交或者回滾事務。

使用訊息服務實現分散式事務在底層的原理上與其他的方法沒有太多的差別,只是訊息服務能夠幫助我們實現的訊息的持久化以及重試等功能,能夠為我們提供一個比較合理的 API 介面,方便開發者使用。

總結

分散式事務的實現方式是分散式系統中非常重要的一個問題,在微服務架構和 SOA 大行其道的今天,掌握分散式事務的原理和使用方式已經是作為後端開發者理所應當掌握的技能,從實現 ACID 事務的 2PC 與 3PC 到實現 BASE 補償式事務的 Saga,再到最後透過事務訊息的方式非同步地保證訊息最終一定會被消費成功,我們為了增加系統的吞吐量以及可用性逐漸降低了系統對一致性的要求。

在業務沒有對一致性有那麼強的需求時,作者一般會使用 Saga 協議對分散式事務進行設計和開發,而在實際工作中,需要強一致性事務的業務場景幾乎沒有,我們都可以實現最終一致性,在發生腦裂或者不一致問題時透過補償的方式進行解決,這就能解決幾乎全部的問題。

【原文連結:】

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31077337/viewspace-2212638/,如需轉載,請註明出處,否則將追究法律責任。

相關文章