對於 MySQL 分佈式事務的見解

一致性理論

當咱們的單個數據庫的性能產生瓶頸的時候,咱們可能會對數據庫進行水平分區,這裏所說的分區指的是物理分區,分區以後可能不一樣的庫就處於不一樣的服務器上了,這個時候單個數據庫的 ACID 已經不能適應這種狀況了,而在這種 ACID 的集羣環境下,再想保證集羣的 ACID 幾乎是很難達到,或者即便能達到那麼效率和性能會大幅降低,最爲關鍵的是再很難擴展新的分區了,這個時候若是再追求集羣的 ACID 會致使咱們的系統變得不好,這時咱們就須要引入一個新的理論原則來適應這種集羣的狀況,就是 CAP 原則或者叫 CAP 定理sql

在分佈式系統中,一致性(Consistency)、可用性(Availability)和分區容忍性(Partition Tolerance)3 個要素最多隻能同時知足兩個,不可兼得。其中,分區容忍性又是不可或缺的。數據庫

一致性模型

數據的一致性模型能夠分紅如下 3 類:
強一致性:數據更新成功後,任意時刻全部副本中的數據都是一致的,通常採用同步的方式實現。
弱一致性:數據更新成功後,系統不承諾當即能夠讀到最新寫入的值,也不承諾具體多久以後能夠讀到。
最終一致性:弱一致性的一種形式,數據更新成功後,系統不承諾當即能夠返回最新寫入的值,可是保證最終會返回上一次更新操做的值。segmentfault

分佈式事務的幾個解決方案

基本全部的分佈式事務都離不開兩階段提交,都是基於兩階段提交的優化。傳統意義的2pc在分佈式環境下具備很是嚴重的侷限性,體如今:服務器

  1. 使用全局事務,數據被鎖住的時間橫跨整個事務,直到事務結束才釋放,在高併發和涉及業務模塊較多的狀況下 對數據庫的性能影響較大。
  2. 在技術棧比較複雜的分佈式應用中,存儲組件可能會不支持 XA 協議。

可是兩階段提交的思想十分常見,InnoDB 存儲引擎中的 Redo log 與 Binlog 的本地事務提交過程也使用了二階段提交的思想,讓這兩個日誌的狀態保持邏輯上的一致。架構

解決方案一:TCC補償模式

TCC 方案是二階段提交的 另外一種實現方式,它涉及 3 個模塊,主業務、從業務和 活動管理器(協做者)併發

第一階段:主業務服務分別調用全部從業務服務的 Try 操做,並在活動管理器中記錄全部從業務服務。當全部從業務服務 Try 成功或者某個從業務服務 Try 失敗時,進入第二階段。app

第二階段:活動管理器根據第一階段從業務服務的 Try 結果來執行 Confirm 或 Cancel 操做。若是第一階段全部從業務服務都 Try 成功,則協做者調用全部從業務服務的 Confirm 操做,不然,調用全部從業務服務的 cancel 操做。異步

在第二階段中,Confirm 和 Cancel 一樣存在失敗狀況,因此須要對這兩種狀況作異常處理以保證數據一致性。
Confirm 失敗:則回滾全部 Confirm 操做並執行 Cancel 操做。
Cancel 失敗:從業務服務須要提供自動重試 Cancel 機制,以保證 Cancel 成功。分佈式

這種方案實現的要素在於,調用鏈須要被記錄,且每一個服務提供者都須要提供一組業務邏輯相反的操做,互爲補償,同時回滾操做和確認提交操做要保證冪等微服務

舉個例子:

Try階段

clipboard.png

Confirm階段

clipboard.png

Cancel階段

clipboard.png

該模式對代碼的嵌入性高,要求每一個業務須要寫三種步驟(Try-Confirm-Cancel)的操做。而且數據一致性的控制幾乎徹底由開發者控制,對業務開發難度要求高,耦合度高,嚴重侵入業務代碼。可是該模式也有必定的好處,對有無本地事務控制的資源層均可以支持,使用面廣。

解決方案二:消息隊列可靠消息提交

以轉帳服務爲例,當支付寶帳戶扣除1萬後,咱們只要生成一個憑證(消息)便可,這個憑證(消息)上寫着「讓餘額寶帳戶增長 1萬」,只要這個憑證(消息)能可靠保存,咱們最終是能夠拿着這個憑證(消息)讓餘額寶帳戶增長1萬的,即咱們能依靠這個憑證(消息)完成最終一致性。最爲核心的問題即是如何可靠的保證消息會被消費以及如何解決消息重複投遞的問題。

如何可靠保存憑證(消息):
  1)支付寶在扣款事務提交以前,向實時消息服務請求發送消息,實時消息服務只記錄消息數據,而不真正發送,只有消息發送成功後纔會提交事務;
  2)當支付寶扣款事務被提交成功後,向實時消息服務確認發送。只有在獲得確認發送指令後,實時消息服務才真正發送該消息;
  3)當支付寶扣款事務提交失敗回滾後,向實時消息服務取消發送。在獲得取消發送指令後,該消息將不會被髮送;
  4)對於那些未確認的消息或者取消的消息,須要有一個消息狀態確認系統定時去支付寶系統查詢這個消息的狀態並進行更新。爲何須要這一步驟,舉個例子:假設在第2步支付寶扣款事務被成功提交後,系統掛了,此時消息狀態並未被更新爲「確認發送」,從而致使消息不能被髮送。
  優勢:消息數據獨立存儲,下降業務系統與消息系統間的耦合;
  缺點:一次消息發送須要兩次請求;業務處理服務須要實現消息狀態回查接口。

如何解決消息重複投遞的問題:
  還有一個很嚴重的問題就是消息重複投遞,以咱們支付寶轉帳到餘額寶爲例,若是相同的消息被重複投遞兩次,那麼咱們餘額寶帳戶將會增長2萬而不是1萬了。
  爲何相同的消息會被重複投遞?好比餘額寶處理完消息msg後,發送了處理成功的消息給實時消息服務,正常狀況下實時消息服務應該要刪除消息msg,但若是實時消息服務這時候悲劇的掛了,重啓後一看消息msg還在,就會繼續發送消息msg。
  解決方法很簡單,消費消息作到冪等性,在餘額寶這邊增長消息應用狀態表(message_apply),通俗來講就是個帳本,用於記錄消息的消費狀況,每次來一個消息,在真正執行以前,先去消息應用狀態表中查詢一遍,若是找到說明是重複消息,丟棄便可,若是沒找到才執行,同時插入到消息應用狀態表(同一事務)。

解決方案三:最大努力通知

核心業務上有 不少附加業務,好比當用戶支付完成後,須要經過短信通知用戶支付成功。這一類業務的成功或者失敗不會影響核心業務,甚至不少大型互聯網平臺在並高併發的狀況下會主動關閉這一類業務以保證核心業務的順利執行。最大努力通知方案就很適合這類業務場景。

最大努力通知方案涉及三個模塊:
上游應用,發消息到 MQ 隊列。
下游應用(例如短信服務、郵件服務),接受請求,並返回通知結果。
最大努力通知服務,監聽消息隊列,將消息按照通知規則調用下游應用的發送通知接口。

具體流程爲下:

  1. 上游應用發送 MQ 消息到 MQ 組件內,消息內包含通知規則和通知地址
  2. 最大努力通知服務監聽到 MQ 內的消息,解析通知規則並放入延時隊列等待觸發通知
  3. 最大努力通知服務調用下游的通知地址,若是調用成功,則該消息標記爲通知成功,若是失敗則在知足通知規則(例如 5 分鐘發一次,共發送 10 次)的狀況下從新放入延時隊列等待下次觸發。

最大努力通知服務表示在 不影響主業務的狀況下,儘量地確保數據的一致性。它須要開發人員根據業務來指定通知規則,在知足通知規則的前提下,儘量的確保數據的一致,以盡到最大努力的目的。

解決方案四:阿里開源分佈式中間件 Seata —— 標準分佈式模型 TXC

對業務無入侵,業務層上無需關心分佈式事務機制的約束,Seata 正是往這個方向發展的。Seata 的設計思路是將一個分佈式事務能夠理解成一個全局事務,下面掛了若干個分支事務,而一個分支事務是一個知足 ACID 的本地事務,所以咱們能夠操做分佈式事務像操做本地事務同樣。

Seata 內部定義了 3個模塊來處理全局事務和分支事務的關係和處理過程,這三個組件分別是:
Transaction Coordinator (TC):事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾。
Transaction Manager (TM):事務的發起者,控制全局事務的邊界,負責開啓一個全局事務,並最終發起全局提交或全局回滾的決議。
Resource Manager (RM):負責控制每一個服務的分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾。

簡要說說整個全局事務的執行步驟:
TM 向 TC 申請開啓一個全局事務,TC 建立全局事務後返回全局惟一的 XID,XID 會在全局事務的上下文中傳播;
RM 向 TC 註冊分支事務,該分支事務歸屬於擁有相同 XID 的全局事務;
TM 向 TC 發起全局提交或回滾;
TC 調度 XID 下的分支事務完成提交或者回滾。

TC,TM,RM三種的概念是否看起來與 XA 是否相像,可是它與 XA 的區別在於,設計了一套不一樣與 XA 的兩階段協議,在保持對業務不侵入的前提下,保證良好的性能,也避免了對底層數據庫協議支持的要求。能夠看做是一套輕量級的 XA 機制。具體的架構層次差異以下:

clipboard.png

XA方案的 RM 其實是在數據庫層,RM本質上就是數據庫自身(經過提供支持 XA 協議的驅動程序來供應用使用)。

而 Seata 的 RM 是以二方包的形式做爲中間件層部署在應用程序這一側的,不依賴與數據庫自己對協議的支持,固然也不須要數據庫支持 XA 協議。這點對於微服務化的架構來講是很是重要的,應用層不須要爲本地事務和分佈式事務兩類不一樣場景來適配兩套不一樣的數據庫驅動。

對比兩個中間層形式的兩階段提交的不一樣,先來看一下 XA 的2PC 過程。

clipboard.png

不管 Phase2 的決議是 commit 仍是 rollback,事務性資源的鎖都要保持到 Phase2 完成才釋放。

再看 Seata 的2PC 過程。

clipboard.png

整個流程中,最爲重要就是 RM,RM 主要是到 TC 控制器端查詢操做的本地數據這一行是否被全局鎖定了,若是被鎖定了,就從新嘗試,若是沒被鎖定,則加全局鎖後開始解析 SQL,把業務數據在更新先後的數據鏡像組織成回滾日誌,並將 undo log 日誌插入 undo_log 表中,保證每條更新數據的業務 sql 都有對應的回滾日誌存在。這樣作的好處就是,本地事務執行完能夠當即釋放本地事務鎖定的資源,而後向 TC 上報分支狀態。當 TM 決議全局提交時,就不須要同步協調處理了,TC 會異步調度各個 RM 分支事務刪除對應的 undo log 日誌和全局鎖,這個步驟很是快速地能夠完成;當 TM 決議全局回滾時,RM 收到 TC 發送的回滾請求,RM 經過 XID 找到對應的 undo log 回滾日誌,而後執行回滾日誌完成回滾操做。

分支事務中數據的 本地鎖 由本地事務管理,在分支事務 Phase1 結束時釋放。同時,隨着本地事務結束,鏈接也得以釋放。
分支事務中數據的 全局鎖 在事務協調器側 TC 管理,在決議 Phase2 全局提交時,全局鎖立刻能夠釋放。只有在決議全局回滾的狀況下,全局鎖才被持有至分支的 Phase2 結束。
這個設計,極大地減小了分支事務對資源(數據和鏈接)的鎖定時間,給總體併發和吞吐的提高提供了基礎。

輔如下圖能夠更快的理解全局鎖以及應用層的 RM 是如何實現的

clipboard.png

總而言之,XA 與 Seata 都存在全局鎖定的過程,可是 Seata 優化了鎖定的機制,Seata 直接本地先提交,減小了分支事務對資源(數據和鏈接)的鎖定時間,釋放了鏈接,數據庫併發和吞吐不會由於全局鎖定而受到影響,由於 Seata 是在應用層去判斷鎖定和等待鎖,而傳統的 XA 則爲數據庫層的全局鎖定。

對分佈式事務的見解

在面臨數據一致性問題的時候,首先要從業務需求的角度出發,肯定咱們對於一致性模型的接受程度,再經過具體場景來決定解決方案。從應用角度看,分佈式事務的現實場景經常沒法規避。在現代技術發展和優化下,結合阿里給出的中間件測試數據,高併發下的分佈式事務也並非沒有可能。

相關文章
相關標籤/搜索