1. 程式人生 > 實用技巧 >分散式事務科普(初識篇)

分散式事務科普(初識篇)

2020-04-10 09:08:00486收藏2 版權

點選上方“朱小廝的部落格”,選擇“設為星標”

後臺回覆"高效Java"領取《Effective Java第三版》

歡迎跳轉到本文的原文連結:https://honeypps.com/architect/introduction-of-distributed-transaction/

《分散式事務科普》是我在YQ期間整理的一篇科普型文章,內容共計兩萬五千字左右,應該算是涵蓋了這個領域的大多數知識點。篇幅較長,遂分為上下兩篇發出。上篇為《分散式事務科普——初識篇》:ACID、事務隔離級別、MySQL事務實現原理、CAP、BASE、2PC、3PC等。下篇為《分散式事務科普——終結篇》,詳細講解分散式事務的解決方案:XA、AT、TCC、Saga、本地訊息表、訊息事務、最大努力通知等(明日放出)。

分散式事務科普

隨著業務的快速發展、業務複雜度越來越高,傳統單體應用逐漸暴露出了一些問題,例如開發效率低、可維護性差、架構擴充套件性差、部署不靈活、健壯性差等等。而微服務架構是將單個服務拆分成一系列小服務,且這些小服務都擁有獨立的程序,彼此獨立,很好地解決了傳統單體應用的上述問題,但是在微服務架構下如何保證事務的一致性呢?本文首先從事務的概念出來,帶大家先回顧一下ACID、事務隔離級別、CAP、BASE、2PC、3PC等基本理論,然後再詳細講解分散式事務的解決方案:XA、AT、TCC、Saga、本地訊息表、訊息事務、最大努力通知等。

什麼是事務

事務提供一種機制,可以將一個活動涉及的所有操作納入到一個不可分割的執行單元,組成事務的所有操作只有在所有操作均能正常執行的情況下方能提交,只要其中任一操作執行失敗,都將導致整個事務的回滾。簡單地說,事務提供一種“要麼什麼都不做,要麼做全套(All or Nothing)”機制。

事務最經典也經常被拿出來說例子就是轉賬了。假如A要給B轉賬1000元,這個轉賬會涉及到兩個關鍵操作就是:將A的餘額減少1000元,將B的餘額增加1000元。萬一在這兩個操作之間突然出現錯誤比如銀行系統崩潰,導致A餘額減少而B的餘額沒有增加,這樣就不對了。事務就是保證這兩個關鍵操作要麼都成功,要麼都要失敗。

事務應該具有4個屬性:原子性、一致性、隔離性、永續性。這四個屬性通常稱為ACID特性。任何事務機制在實現時,都應該考慮事務的ACID特性,包括:本地事務、分散式事務,即使不能都很好的滿足,也要考慮支援到什麼程度。

ACID

ACID 理論是對事務特性的抽象和總結,方便我們實現事務。你可以理解成:如果實現了操作的 ACID 特性,那麼就實現了事務。ACID的具體含義詳述如下。

原子性(Atomicity):原子性是指單個事務本身涉及到的資料庫操作,要麼全部成功,要麼全部失敗,不存在完成事務中一部分操作的可能。以上文說的轉賬為例,就是要麼操作全部成功,A的錢少了,B的錢多了;要麼就是全部失敗,AB保持和原來一直的數目。

一致性(Consistency):事務必須是使資料庫從一個一致性狀態變到另一個一致性狀態,事務的中間狀態不能被觀察到的。還是以轉賬為例,原來AB賬戶的錢加一起是1000,相互轉賬完成時候彼此還是1000,所以一致性理解起來就是事務執行前後的資料狀態是穩定的,對於轉賬就是金額穩定不變,對於其他的事務操作就是事務執行完成之後,資料庫的狀態是正確的,沒有髒資料。

隔離性(isolation):一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。隔離性側重於多個事務之間的特性,也就是說多個事務之間是沒有相互影響的比如A給B轉賬和B給C轉賬這兩個事務是沒有影響的(這裡B給C轉賬如果和A給B轉賬的事務同時進行的時候,B的金額正確性問題保證就要看隔離級別了)。

永續性(durability):永續性也稱永久性(permanence),指一個事務一旦提交,它對資料庫中資料的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

事務的隔離級別

在多個事務併發操作資料庫(多執行緒、網路併發等)的時候,如果沒有有效的避免機制,就會出現髒讀、不可重複讀和幻讀這3種問題。

髒讀(Dirty Read)

A事務讀取B事務尚未提交的資料,此時如果B事務由於某些原因執行了回滾操作,那麼A事務讀取到的資料就是髒資料。

參考下圖,事務A讀取到了事務B未提交的記錄。

不可重複讀(Nonrepeatable Read)

一個事務內前後多次讀取,資料內容不一致。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩次讀資料之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重複讀。

參考下圖,事務A讀取到的name可能為“張三”,也可能為“李四”。

幻讀(Phantom Read)

一個事務內前後多次讀取,資料總量不一致。參考下圖,事務A在執行讀取操作,需要兩次統計資料的總量,前一次查詢資料總量後,此時事務B執行了新增資料的操作並提交後,這個時候事務A讀取的資料總量和之前統計的不一樣,就像產生了幻覺一樣,平白無故的多了幾條資料,成為幻讀。

不可重複讀和幻讀有些相似,兩者的區別在於:不可重複讀的重點在於修改,同樣的條件, 你讀取過的資料,再次讀取出來發現值不一樣了;而幻讀的重點在於新增或者刪除(參考MySQL官網https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html對幻讀的定義,記錄的減少也應該算是幻讀),同樣的條件, 第 1 次和第 2 次讀出來的記錄數不一樣。

隔離級別

事務的隔離性是指多個併發的事務同時訪問一個數據庫時,一個事務不應該被另一個事務所幹擾,每個併發的事務間要相互進行隔離。SQL 標準定義了以下四種隔離級別:

  • 讀未提交(Read Uncommitted):一個事務可以讀取到另一個事務未提交的修改。這種隔離級別是最弱的,可能會產生髒讀,幻讀,不可重複讀的問題問題。

  • 讀已提交(Read Committed):一個事務只能讀取另一個事務已經提交的修改。其避免了髒讀,仍然存在不可以重複讀和幻讀的問題。SQL Server和Oracle的預設隔離級別就是這個。

  • 可重複讀(Repeated Read):同一個事務中多次讀取相同的資料返回的結果是一樣的。其避免了髒讀和不可重複讀問題,但是幻讀依然存在。MySQL中的預設隔離級別就是這個,不過MySQL通過多版本併發控制(MVCC)、Next-key Lock等技術解決了幻讀問題。

  • 序列化(Serializable):這是資料庫最高的隔離級別,這種級別下,事務“序列化順序執行”,也就是一個一個排隊執行。在這種級別下,髒讀、不可重複讀、幻讀都可以被避免,但是執行效率奇差,效能開銷也最大。

事務的隔離級別和髒讀、不可重複讀、幻讀的關係總結如下表所示:

隔離級別髒讀不可重複讀幻讀
未提交讀 可能 可能 可能
已提交讀 不可能 可能 可能
可重複讀 不可能 不可能 可能
可序列化 不可能 不可能 不可能

MySQL事務實現原理

這裡所說的MySQL事務是指使用InnoDB引擎時的事務。MySQL在5.5版本之前預設的資料庫引擎時MyISAM,雖然效能極佳,而且提供了大量的特性,包括全文索引、壓縮、空間函式等,但MyISAM不支援事務和行級鎖,而且最大的缺陷就是崩潰後無法安全恢復。5.5版本之後,MySQL引入了InnoDB(事務性資料庫引擎),MySQL 5.5版本後預設的儲存引擎為InnoDB。

redo log和undo log來保證事務的原子性、一致性和永續性,同時採用預寫日誌(WAL)方式將隨機寫入變成順序追加寫入,提升事務效能。而隔離性是通過鎖技術來保證的。

這裡我們不放先來了解一下redo log和undo log。redo log是重做日誌,提供前滾操作,undo log是回滾日誌,提供回滾操作。undo log不是redo log的逆向過程,其實它們都算是用來恢復的日誌:

  • redo log通常是物理日誌,記錄的是資料頁的物理修改,而不是某一行或某幾行修改成怎樣怎樣,它用來恢復提交後的物理資料頁(恢復資料頁,且只能恢復到最後一次提交的位置)。

  • undo用來回滾行記錄到某個版本。undo log一般是邏輯日誌,根據每行記錄進行記錄。

redo log

redo log 又稱為重做日誌,它包含兩部分:一是記憶體中的日誌緩衝(redo log buffer),該部分日誌是易失性的;二是磁碟上的重做日誌檔案(redo log file),該部分日誌是持久的。

當需要修改事務中的資料時,先將對應的redo log寫入到redo log buffer中,然後才在記憶體中執行相關的資料修改操作。InnoDB通過“force log at commit”機制實現事務的永續性,即在事務提交的時候,必須先將該事務的所有redo log都寫入到磁碟上的redo log file中,然後待事務的commit操作完成才算整個事務操作完成。

在每次將redo log buffer中的內容寫入redo log file時,都需要呼叫一次fsync操作,以此確保redo log成功寫入到磁碟上(參考下圖,內容的流向為:使用者態的記憶體->作業系統的頁快取->物理磁碟)。因此,磁碟的效能在一定程度上也決定了事務提交的效能。這裡還可以通過innodb_flush_log_at_trx_commit來控制redo log刷磁碟的策略,這裡就不做贅述了。

undo log

undo log有2個功能:實現回滾和多版本併發控制(MVCC, Multi-Version Concurrency Control)。

在資料修改的時候,不僅記錄了redo log,還記錄了相對應的undo log,如果因為某些原因導致事務失敗或回滾了,可以藉助該undo log進行回滾。

undo log和redo log記錄物理日誌不一樣,它是邏輯日誌。可以認為當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。

當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾。有時候應用到行版本控制的時候,也是通過undo log來實現的:當讀取的某一行被其他事務鎖定時,它可以從undo log中分析出該行記錄以前的資料是什麼,從而提供該行版本資訊,讓使用者實現非鎖定一致性讀取。

MVCC

說到undo log,就不得不順帶提一下MVCC了,因為MVCC的實現依賴了undo log。當然,MVCC的實現還依賴了隱藏欄位(DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID)、Read View等。

MVCC的全稱是多版本併發控制,它使得在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務下執行一致性讀操作有了保證。換言之,就是為了查詢一些正在被另一個事務更新的行,並且可以看到它們被更新之前的值。這是一個可以用來增強併發性的強大技術,因為這樣的一來的話查詢就不用等待另一個事務釋放鎖,使不同事務的讀-寫、寫-讀操作併發執行,從而提升系統性能。

這裡的讀指的是“快照讀”。普通的SELECT操作就是快照讀,有的地方也稱之為“一致性讀”或者“一致性無鎖讀”。它不會對錶中的任何記錄做加鎖動作,即不加鎖的非阻塞讀。快照讀的前提是隔離級別不是序列化級別,序列化級別下的快照讀會退化成當前讀。之所以出現快照讀的情況,是基於提高併發效能的考慮,這裡可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷。當然,既然是基於多版本,即快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本。

對應的還有“當前讀”。類似UPDATE、DELETE、INSERT、SELECT...LOCK IN SHARE MODE、SELECT...FOR UPDATE這些操作就是當前讀。為什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

鎖技術

併發事務的讀-讀情況並不會引起什麼問題(讀取操作本身不會對記錄有任何影響,並不會引起什麼問題,所以允許這種情況的發生),不過對於寫-寫、讀-寫或寫-讀這些情況可能會引起一些問題,需要使用MVCC或者加鎖的方式來解決它們。

在使用加鎖的方式解決問題時,既要允許讀-讀情況不受影響,又要使寫-寫、讀-寫或寫-讀情況中的操作相互阻塞。這裡引入了兩種行級鎖:

  • 共享鎖:英文名為Shared Locks,簡稱S鎖。允許事務讀一行資料。

  • 排它鎖:也常稱獨佔鎖,英文名為Exclusive Locks,簡稱X鎖。允許事務刪除或更新一行資料。

假如事務A首先獲取了一條記錄的S鎖之後,事務B接著也要訪問這條記錄:1) 如果事務B想要再獲取一個記錄的S鎖,那麼事務B也會獲得該鎖,也就意味著事務A和B在該記錄上同時持有S鎖;2) 如果事務B想要再獲取一個記錄的X鎖,那麼此操作會被阻塞,直到事務A提交之後將S鎖釋放掉。

如果事務A首先獲取了一條記錄的X鎖之後,那麼不管事務B接著想獲取該記錄的S鎖還是X鎖都會被阻塞,直到事務A提交。

除了 S鎖 和 S 鎖相容,其他都不相容。

InnoDB儲存引擎還支援多粒度鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。為此,InnoDB儲存引擎引入了意向鎖(表級別鎖):

  • 意向共享鎖(IS 鎖):事務想要獲取一張表的幾行資料的共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖。

  • 意向排他鎖(IX 鎖):事務想要獲取一張表中幾行資料的排它鎖,事務在給一個數據行加排它鎖前必須先取得該表的 IX 鎖。

當我們在對使用InnoDB儲存引擎的表的某些記錄加S鎖之前,那就需要先在表級別加一個IS鎖,當我們在對使用InnoDB儲存引擎的表的某些記錄加X鎖之前,那就需要先在表級別加一個IX鎖。IS鎖和IX鎖的使命只是為了後續在加表級別的S鎖和X鎖時判斷表中是否有已經被加鎖的記錄,以避免用遍歷的方式來查看錶中有沒有上鎖的記錄。

下表展示了X、IX、S、IS鎖的相容性:

相容性XIXSIS
X 不相容 不相容 不相容 不相容
IX 不相容 相容 不相容 相容
S 不相容 不相容 相容 相容
IS 不相容 相容 相容 相容

這裡還要了解一下的是,在InnoDB中有 3 種行鎖的演算法:

  • Record Locks(記錄鎖):單個行記錄上的鎖。

  • Gap Locks(間隙鎖):在記錄之間加鎖,或者在第一個記錄之前加鎖,亦或者在最後一個記錄之後加鎖,即鎖定一個範圍,而非記錄本身。

  • Next-Key Locks:結合 Gap Locks 和 Record Locks,鎖定一個範圍,並且鎖定記錄本身。主要解決的是 REPEATABLE READ 隔離級別下的幻讀問題。

對於Next-Key Locks,如果我們鎖定了一個行,且查詢的索引含有唯一屬性時(即有唯一索引),那麼這個時候InnoDB會將Next-Key Locks優化成Record Locks,也就是鎖定當前行,而不是鎖定當前行加一個範圍;如果我們使用的不是唯一索引鎖定一行資料,那麼此時InnoDB就會按照本來的規則鎖定一個範圍和記錄。還有需要注意的點是,當唯一索引由多個列組成時,如果查詢僅是查詢其中的一個列,這時候是不會降級的。InnoDB儲存引擎還會對輔助索引的下一個鍵值區間加Gap Locks(這麼做也是為了防止幻讀)。

總結

MySQL實現事務ACID特性的方式總結如下:

  • 原子性:使用 undo log來實現,如果事務執行過程中出錯或者使用者執行了rollback,系統通過undo log日誌返回事務開始的狀態。

  • 永續性:使用 redo log來實現,只要redo log日誌持久化了,當系統崩潰,即可通過redo log把資料恢復。

  • 隔離性:通過鎖以及MVCC來實現。

  • 一致性:通過回滾、恢復以及併發情況下的隔離性,從而實現一致性。

理論基石

對於事務,想必大家也或多或少地聽到過類似本地事務、資料庫事務、傳統事務、剛性事務、柔性事務、分散式事務等多種稱呼(還有如單機事務、全域性事務等稱呼),那麼這些多種類的事務分別指的是什麼呢?

本地事務(Local Transaction),通常也被稱之為資料庫事務、傳統事務(相對於分散式事務而言)。它僅限於對單一資料庫資源的訪問控制,如下圖(左)所示。

不過,現在隨著系統架構的服務化,事務的概念也延伸到了服務中,倘若將一個單一的服務操作作為一個事務,那麼整個服務操作只能涉及一個單一的資料庫資源。由此,本地事務的定義可以擴充套件為基於單個服務單一資料庫資源訪問的事務,如上圖(右)所示。

本地事務通常由資源管理器進行管理,如下圖所示。

本地事務的優點就是支援嚴格的ACID特性,高效,可靠,狀態可以只在資源管理器中維護,而且應用程式設計模型簡單。但是本地事務不具備分散式事務的處理能力,隔離的最小單位受限於資源管理器。

與傳統的本地事務所對應的是分散式事務,它指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分佈在不同的伺服器上,且屬於不同的應用,分散式事務需要保證這些小操作要麼全部成功,要麼全部失敗。

剛性事務是指完全遵循ACID規範的事務。最常見的剛性事務就是資料庫事務(本地事務),比如MySQL事務就是一種典型的剛性事務。

在電商領域等網際網路場景下,傳統的事務在資料庫效能和處理能力上都暴露出了瓶頸。在分散式領域基於CAP理論以及BASE理論,有人就提出了柔性事務的概念。柔性事務為了滿足可用性、效能與降級服務的需要,降低了一致性(Consistency)與隔離性(Isolation)的要求。

CAP

CAP是指的是在一個分散式系統中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可得兼。

  • 一致性(C):每次讀取要麼獲得最近寫入的資料,要麼獲得一個錯誤。

  • 可用性(A):每次請求都能獲得一個(非錯誤)響應,但不保證返回的是最新寫入的資料。

  • 分割槽容忍性(P):儘管任意數量的訊息被節點間的網路丟失(或延遲),系統仍繼續執行。

ACID理論和CAP理論都有一個C,也都叫一致性, 所以很多人都會把這兩個概念當做是一個概念。不過,這兩個C是有區別的:

  • ACID的C指的是事務中的一致性,在一系列對資料修改的操作中,保證資料的正確性。即資料在事務期間的多個操作中,資料不會憑空的消失或增加,資料的每一個增刪改操作都是有因果關係的。比如使用者A向用戶B轉了200塊錢,不會出現使用者A扣了款,而使用者B沒有收到的情況。

  • 在分散式環境中,多服務之間的複製是非同步,需要一定耗時,不會瞬間完成。在某個服務節點的資料修改之後,到同步到其它服務節點之間存在一定的時間間隔,如果在這個間隔內有併發讀請求過來,而這些請求又負載均衡到多個節點,可能會出現從多個節點資料不一致的情況,因為請求有可能會落到還沒完成資料同步的節點上。CAP中的C就是為了做到在分散式環境中讀取的資料是一致的。

總的來說,ACID的C著重強調單資料庫事務操作時,要保證資料的完整和正確性,而CAP理論中的C強調的是對一個數據多個備份的讀寫一致性。

有關CAP的更多解讀,可以看看這篇《越說越迷糊的CAP》。記得關注公眾號:朱小廝的部落格。

BASE

在 CAP 理論中,三者不可同時滿足,而服務化中,更多的是提升 A 以及 P,在這個過程中不可避免的會降低對 C 的要求,因此,BASE 理論隨之而來。

BASE理論來源於 ebay 在 2008 年 ACM 中發表的論文(下載地址:https://dl.acm.org/doi/10.1145/1394127.1394128),BASE 理論的基本原則有三個:Basically Available(基本可用)、Soft state(軟狀態)和 Eventually consistent(最終一致性),主要目的是為了提升分散式系統的可伸縮性,論文同樣闡述瞭如何對業務進行調整以及折中的手段,BASE 理論的提出為分散式事務的發展指出了一個方向。

BASE理論是對CAP中一致性和可用性權衡的結果,其來源於對大規模網際網路系統分散式實踐的總結,是基於CAP定理逐步演化而來的。BASE理論的核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性。BASE 理論本質上是對 CAP 理論的延伸,是對 CAP 中 AP 方案的一個補充。

基本可用

基本可用是指分散式系統在出現不可預知故障的時候,允許損失部分可用性。不過,這絕不等價於系統不可用。比如:

  1. 響應時間上的損失。正常情況下,一個線上搜尋引擎需要在0.5秒之內返回給使用者相應的查詢結果,但由於出現故障,查詢結果的響應時間增加了1~2秒。

  2. 系統功能上的損失:正常情況下,在一個電子商務網站上進行購物的時候,消費者幾乎能夠順利完成每一筆訂單,但是在一些節日大促購物高峰的時候,由於消費者的購物行為激增,為了保護購物系統的穩定性,部分消費者可能會被引導到一個降級頁面。

軟狀態

軟狀態,也被稱之為柔性狀態,是指允許系統中的資料存在中間狀態,並認為該中間狀態的存在不會影響系統的整體可用性,即允許系統在不同節點的資料副本之間進行資料同步的過程存在延時。

最終一致性

最終一致性強調的是所有的資料副本,在經過一段時間的同步之後,最終都能夠達到一個一致的狀態。因此,最終一致性的本質是需要系統保證最終資料能夠達到一致,而不需要實時保證系統資料的強一致性。

分散式事務的由來

當下網際網路發展如火如荼,絕大部分公司都進行了資料庫拆分和服務化(SOA)。在這種情況下,完成某一個業務功能可能需要橫跨多個服務,操作多個數據庫。這就涉及到了分散式事務,用需要操作的資源位於多個資源伺服器上,而應用需要保證對於多個資源伺服器的資料的操作,要麼全部成功,要麼全部失敗。本質上來說,分散式事務就是為了保證不同資源伺服器的資料一致性。

最早的分散式事務應用架構很簡單,不涉及服務間的訪問呼叫,僅僅是服務內操作涉及到對多個數據庫資源的訪問,如下圖所示。

當一個服務操作訪問不同的資料庫資源,又希望對它們的訪問具有事務特性時,就需要採用分散式事務來協調所有的事務參與者。

典型的應用場景如分表分庫中的事務。通常一個庫資料量比較大或者預期未來的資料量比較大,都會進行水平拆分,也就是分庫分表。對於分庫分表的情況,一般開發人員都會使用一些資料庫中介軟體來降低SQL操作的複雜性。如:INSERT INTO user(id, name) VALUES (1,"張三"),(2,"李四");這條SQL是操作單庫的語法,單庫情況下,可以保證事務的一致性。但是由於現在進行了分庫分表,開發人員希望將1號記錄插入分庫1,2號記錄插入分庫2。所以資料庫中介軟體要將其改寫為2條SQL,分別插入兩個不同的分庫,此時要保證兩個庫要不都成功,要不都失敗,因此基本上所有的資料庫中介軟體都面臨著分散式事務的問題。

對於上面介紹的分散式事務應用架構,儘管一個服務操作會訪問多個數據庫資源,但是畢竟整個事務還是控制在單一服務的內部。如果一個服務操作需要呼叫另外一個服務,這時的事務就需要跨越多個服務了。在這種情況下,起始於某個服務的事務在呼叫另外一個服務的時候,需要以某種機制流轉到另外一個服務,從而使被呼叫的服務訪問的資源也自動加入到該事務當中來。下圖反映了這樣一個跨越多個服務的分散式事務。

舉個簡單的例子,一個公司之內,使用者的資產可能分為好多個部分,比如餘額,積分,優惠券等等,如下圖所示。

在公司內部有可能積分功能由一個微服務團隊維護,優惠券又是另外的團隊維護,這樣的話就無法保證積分扣減了之後,優惠券能否扣減成功。所以這裡也需要使用分散式事務來控制。

如果將上面這兩種場景(一個服務可以呼叫多個數據庫資源,也可以呼叫其他服務)結合在一起,對此進行延伸,整個分散式事務的參與者將會組成如下圖所示的樹形拓撲結構。在一個跨服務的分散式事務中,事務的發起者和提交均系同一個,它可以是整個呼叫的客戶端,也可以是客戶端最先呼叫的那個服務。

上述討論的分散式事務場景中,無一例外的都直接或者間接的操作了多個數據庫。如何保證事務的ACID特性,對於分散式事務實現方案而言,是非常大的挑戰。同時,分散式事務實現方案還必須要考慮效能的問題,如果為了嚴格保證ACID特性,導致效能嚴重下降,那麼對於一些要求快速響應的業務,是無法接受的。

分散式事務一致性協議

如果一個操作涉及多個分散式節點,為了保證事務的ACID特性,需要引入一個“協調者”元件來統一排程所有分散式節點的執行邏輯,這些被排程的分散式節點被稱為“參與者”。協調者負責排程參與者的行為,並最終決定這些參與者是否真正地提交事務。

分散式事務通常採用二階段提交協議(2PC),它是幾乎所有分散式事務演算法的基礎,後續的分散式事務演算法幾乎都由此改進而來。

2PC

二階段提交(Two-phase Commit,簡稱2PC),是指為了使基於分散式系統架構下的所有節點在進行事務提交時保持一致性而設計的一種演算法(Algorithm)。通常2PC也被稱為是一種協議(Protocol)。

在此協議中,一個事務管理器(Transaction Manager,簡稱 TM,也被稱之為“協調者”)協調 1 個或多個資源管理器(Resource Manager,簡稱 RM,也被稱之為“參與者”)的活動,所有資源管理器(參與者)向事務管理器(協調者)彙報自身活動狀態,由事務管理器(協調者)根據各資源管理器(協調者)彙報的狀態(完成準備或準備失敗)來決定各資源管理器(協調者)是“提交”事務還是進行“回滾”操作。

因此,二階段提交的演算法思路可以概括為:參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。

所謂的兩個階段是指:第一階段:準備階段(投票階段)和第二階段:提交階段(執行階段)。

第一階段

協調者通知各個參與者準備提交它們的事務分支。如果參與者判斷自己進行的工作可以被提交,那就對工作內容進行持久化,再給協調者肯定答覆;要是發生了其他情況,那給協調者的都是否定答覆。在傳送了否定答覆並回滾了已經的工作後,參與者就可以丟棄這個事務分支資訊。

以MySQL資料庫為例,在第一階段,事務管理器(協調者)向所有涉及到的資料庫伺服器(參與者)發出Prepare "準備提交"請求,資料庫(參與者)收到請求後執行資料修改和日誌記錄等處理,處理完成後只是把事務的狀態改成"可以提交",然後把結果返回給事務管理器(協調者)。

第二階段

協調者根據第一階段中各個參與者 Prepare的結果,決定是提交還是回滾事務。如果所有的參與者都Prepare成功,那麼協調者通知所有的參與者進行提交;如果有參與者Prepare失敗的話,則協調者通知所有參與者回滾自己的事務分支。

還是以MySQL資料庫為例,如果第一階段中所有資料庫(參與者)都Prepare成功,那麼事務管理器(協調者)向資料庫伺服器(參與者)發出"確認提交"請求,資料庫伺服器(參與者)把事務的"可以提交"狀態改為"提交完成"狀態,然後返回應答。如果在第一階段內有任何一個數據庫(參與者)的操作發生了錯誤,或者事務管理器(協調者)收不到某個資料庫(參與者)的迴應,則認為事務失敗,回撤所有資料庫(參與者)的事務。資料庫伺服器(參與者)收不到第二階段的確認提交請求,也會把"可以提交"的事務回撤。

2PC提交的優點是儘量保證了資料的強一致,但不是 100% 一致。但是2PC也有明顯的缺點:

  • 單點故障:由於協調者的重要性,一旦協調者發生故障,參與者會一直阻塞,尤其是在第二階段,協調者發生故障,那麼所有的參與者都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。

  • 同步阻塞:由於所有節點在執行操作時都是同步阻塞的,當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。

  • 資料不一致:在第二階段中,當協調者向參與者傳送提交事務請求之後,發生了局部網路異常或者在傳送提交事務請求過程中協調者發生了故障,這會導致只有一部分參與者接收到了提交事務請求。而在這部分參與者接到提交事務請求之後就會執行提交事務操作。但是其他部分未接收到提交事務請求的參與者則無法提交事務。從而導致分散式系統中的資料不一致。

二階段提交的問題:如果協調者在第二階段傳送提交請求之後掛掉,而唯一接受到這條訊息的參與者執行之後也掛掉了,即使協調者通過選舉協議產生了新的協調者並通知其他參與者進行提交或回滾操作的話,都可能會與這個已經執行的參與者執行的操作不一樣。

3PC

三階段提交(Three-phase Commit,簡稱3PC),是為解決2PC中的缺點而設計的。參考維基百科:https://en.wikipedia.org/wiki/Three-phasecommitprotocol。與兩階段提交不同的是,三階段提交是“非阻塞”協議。

對應於2PC,3PC有兩個改動點:

  1. 引入超時機制。同時在協調者和參與者中都引入超時機制。

  2. 在兩階段提交的第一階段與第二階段之間插入了一個準備階段,使得原先在兩階段提交中,參與者在投票之後,由於協調者發生崩潰或錯誤而導致參與者處於無法知曉是否提交或者中止的“不確定狀態”所產生的可能相當長的延時的問題得以解決。

第一階段CanCommit

3PC的CanCommit階段其實和2PC的準備階段很像。協調者向參與者傳送Commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。

  1. 事務詢問:協調者向參與者傳送CanCommit請求。詢問是否可以執行事務提交操作。然後開始等待參與者的響應。

  2. 響應反饋:參與者接到CanCommit請求之後,正常情況下,如果其自身認為可以順利執行事務,則返回Yes響應,並進入預備狀態。否則反饋No。

第二階段PreCommit

協調者根據參與者的反應情況來決定是否可以進行事務的PreCommit操作。根據響應情況,有以下兩種可能。

  • 假如協調者從所有的參與者獲得的反饋都是Yes響應,那麼就會執行事務的預執行。

    a) 傳送預提交請求:協調者向參與者傳送PreCommit請求,並進入Prepared階段。

    b) 事務預提交:參與者接收到PreCommit請求後,會執行事務操作,並將undo和redo資訊記錄到事務日誌中。

    c) 響應反饋:如果參與者成功的執行了事務操作,則返回ACK響應,同時開始等待最終指令。

  • 假如有任何一個參與者向協調者傳送了No響應,或者等待超時之後,協調者都沒有接到參與者的響應,那麼就執行事務的中斷。

    a) 傳送中斷請求:協調者向所有參與者傳送Abort請求。

    b) 中斷事務:參與者收到來自協調者的Abort請求之後(或超時之後,仍未收到協調者的請求),執行事務的中斷。

第三階段DoCommit

該階段進行真正的事務提交,也可以分為以下兩種情況。

  • Case 1:執行提交。

    a) 傳送提交請求:協調者接收到參與者傳送的ACK響應,那麼他將從預提交狀態進入到提交狀態。並向所有參與者傳送DoCommit請求。

    b) 事務提交:參與者接收到DoCommit請求之後,執行正式的事務提交。並在完成事務提交之後釋放所有事務資源。

    c) 響應反饋:事務提交完之後,向協調者傳送ACK響應。

    d) 完成事務:協調者接收到所有參與者的ACK響應之後,完成事務。

  • Case 2:中斷事務。協調者沒有接收到參與者傳送的ACK響應(可能是接受者傳送的不是ACK響應,也可能響應超時),那麼就會執行中斷事務。

    a) 傳送中斷請求:協調者向所有參與者傳送Abort請求。

    b) 事務回滾:參與者接收到Abort請求之後,利用其在階段二記錄的undo資訊來執行事務的回滾操作,並在完成回滾之後釋放所有的事務資源。

    c) 反饋結果:參與者完成事務回滾之後,向協調者傳送ACK訊息。

    d) 中斷事務:協調者接收到參與者反饋的ACK訊息之後,執行事務的中斷。

在三階段提交中,如果在第三階段協調者傳送提交請求之後掛掉,並且唯一的接受的參與者執行提交操作之後也掛掉了,這時協調者通過選舉協議產生了新的協調者。在二階段提交時存在的問題就是新的協調者不確定已經執行過事務的參與者是執行的提交事務還是中斷事務。但是在三階段提交時,肯定得到了第二階段的再次確認,那麼第二階段必然是已經正確的執行了事務操作,只等待提交事務了。所以新的協調者可以從第二階段中分析出應該執行的操作,進行提交或者中斷事務操作,這樣即使掛掉的參與者恢復過來,資料也是一致的。

所以,三階段提交解決了二階段提交中存在的由於協調者和參與者同時掛掉可能導致的資料一致性問題和單點故障問題,並減少阻塞。因為一旦參與者無法及時收到來自協調者的資訊之後,他會預設執行提交事務,而不會一直持有事務資源並處於阻塞狀態。

不過3PC也存在自身的問題:在提交階段如果傳送的是中斷事務請求,但是由於網路問題,導致部分參與者沒有接到請求。那麼參與者會在等待超時之後執行提交事務操作,這樣這些由於網路問題導致提交事務的參與者的資料就與接受到中斷事務請求的參與者存在資料不一致的問題。所以無論是 2PC 還是 3PC 都不能保證分散式系統中的資料 100% 一致。

分散式事務解決方案

預告目錄如下(明日放):

想知道更多?掃描下面的二維碼關注我

歡迎跳轉到本文的原文連結:https://honeypps.com/architect/introduction-of-distributed-transaction/