一文徹底搞透分布式一致性
分布式系統(tǒng)下的數(shù)據(jù)一致性可以分為兩大類:
- 事務一致性:當多個節(jié)點進行操作時,所有節(jié)點最終達成的狀態(tài)都是一致的。這需要通過協(xié)調(diào)來保證操作的正確性,避免出現(xiàn)數(shù)據(jù)不一致的情況;
- 副本一致性:數(shù)據(jù)的多個副本之間保持一致性,這需要保證在對數(shù)據(jù)進行修改時,所有副本都能夠及時更新,避免數(shù)據(jù)出現(xiàn)不同步的情況;
定義都比較抽象,舉個例子感受一下:
- 事務一致性:電商平臺使用優(yōu)惠券下單場景:
圖片
- 下單成功,優(yōu)惠券必須處于“已鎖定”狀態(tài);
- 支付成功,優(yōu)惠券必須處于“已使用”狀態(tài);
- 訂單取消,優(yōu)惠券需要恢復為“待使用”狀態(tài);
- 優(yōu)惠券和訂單間就屬于“事務一致”,兩者間存在強關聯(lián)關系。
- 副本一致性:
- MySQL 主從復制:是指在主數(shù)據(jù)庫上進行數(shù)據(jù)操作后,將這些操作同步到一個或多個從數(shù)據(jù)庫上。從庫必須與主庫保持同步,以便從庫中的數(shù)據(jù)和主庫中的數(shù)據(jù)保持一致;
- Redis 與 MySQL 一致性:在將 Redis 作為存儲使用時,可以將 MySQL 看做主節(jié)點,Redis 看做從節(jié)點,當 MySQL 數(shù)據(jù)發(fā)生變更時,自動同步到 Redis 中,并保持數(shù)據(jù)的一致性;
image
【注】本文著重介紹 “事務一致性”,多副本一致性,詳見 緩存 或 ES 篇。
1. 脫離數(shù)據(jù)庫事務的懷抱
在關系型數(shù)據(jù)庫中,事務(Transaction)是指一組數(shù)據(jù)庫操作,這些操作要么全部成功要么全部失敗。事務可以保證某些數(shù)據(jù)操作的一致性,當某一條操作失敗時,會進行回滾,即撤銷已執(zhí)行的操作,使數(shù)據(jù)恢復到操作前的狀態(tài)。
提到事務一致性,不得不說數(shù)據(jù)庫事務 ACID:ACID是指數(shù)據(jù)庫事務的四個關鍵特性,分別為原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability):
- 原子性(Atomicity):事務應該被視為一個原子操作,即事務中的所有操作要么全部執(zhí)行成功,要么全部失敗回滾。如果事務執(zhí)行過程中出現(xiàn)錯誤,所有修改操作將被回滾撤銷,不會對數(shù)據(jù)造成損壞;
- 一致性(Consistency):事務執(zhí)行前后,數(shù)據(jù)應該保持一致狀態(tài)。所有數(shù)據(jù)修改操作都必須確保數(shù)據(jù)庫的約束條件、觸發(fā)器等規(guī)則不會被破壞,保持數(shù)據(jù)完整性;
- 隔離性(Isolation):多個事務同時對同一數(shù)據(jù)進行操作時,事務之間應該相互隔離,互不干擾。數(shù)據(jù)庫系統(tǒng)應該確保在并發(fā)情況下,事務的執(zhí)行結果和串行執(zhí)行的結果一致;
- 持久性(Durability):事務完成后,其對數(shù)據(jù)庫所作的所有修改都應該被永久保存,即使系統(tǒng)崩潰或重啟后,修改的數(shù)據(jù)也應該是可用的;
銀行轉賬應用程序就是典型的 ACID 模型的應用場景。假設用戶A要向用戶B轉賬1000元,轉賬過程就是一個事務,具有原子性、一致性、隔離性和持久性四大特性:
- 原子性:轉賬過程總共涉及兩個操作:從A賬戶中減去1000元,向B賬戶中加上1000元。如果這兩個操作中的任何一個失敗,整個事務都將失敗回滾;
- 一致性:轉賬前后所有賬戶的余額總和應該是不變的,不會出現(xiàn)余額不足或超額的情況;
- 隔離性:如果同時發(fā)起兩個轉賬事務,應該確保每個事務只訪問自己的數(shù)據(jù),不會互相干擾;
- 持久性:一旦轉賬完成,更改數(shù)據(jù)的事務就必須寫入磁盤,保證即使系統(tǒng)崩潰或重啟后,這些數(shù)據(jù)仍然是可用的;
數(shù)據(jù)庫事務絕對是程序員的一大利器,但由于各種原因,這把利器離我們越來越遠:
- 負載的挑戰(zhàn):隨著業(yè)務的快速增長,數(shù)據(jù)庫中的數(shù)據(jù)量或負載也會達到單一實例的上線,此時,我們:
垂直拆分:將不同的表放到不同的數(shù)據(jù)庫實例,比如拆分出 User 實例,Order 實例;
圖片
水平拆分:數(shù)據(jù)量超過單表最大容量時,將數(shù)據(jù)分拆到不同的數(shù)據(jù)庫,比如 Order-1 實例、Order-2 實例;
圖片
垂直+水平拆分:先進行垂直拆分,在進行水平拆分;
圖片
- 微服務的挑戰(zhàn):微服務已經(jīng)成為系統(tǒng)的事實架構,特別是 Spring Boot 和 Spring Cloud 的流行:
微服務的“自治”要求每個微服務都應該有自己的獨立數(shù)據(jù)存儲,避免與其他服務共享數(shù)據(jù)存儲,從而降低服務之間的耦合性;
微服務間通過服務發(fā)現(xiàn)、負載均衡等方式,將服務之間的關系解耦,從而使得每個服務都具備獨立的自治性;
圖片
不管觸發(fā)哪一種條件,都會產(chǎn)生跨數(shù)據(jù)庫事務,從而增加系統(tǒng)設計的難度。
2. 常見一致性保障機制
針對該問題前人已經(jīng)提出來多種應對方案,特別是關系型數(shù)據(jù)庫。
2.1. MySQL事務一致性
熟悉 MySQL 實現(xiàn)的伙伴知道,MySQL 是通過 Redo log 和 Undo log 來實現(xiàn)事務一致性的:
- Redo Log:Redo Log 記錄了事務對數(shù)據(jù)庫所作的修改,包括插入、更新、刪除等操作,它在事務提交前就被寫入磁盤。如果出現(xiàn)故障導致系統(tǒng)崩潰,MySQL 會從 Redo Log 中恢復數(shù)據(jù);
- Undo Log:Undo Log 記錄了事務對數(shù)據(jù)庫所作的修改的「前置操作」,并且在事務回滾時用來撤銷事務所做的修改。當事務執(zhí)行更新時,MySQL 會先將修改前的數(shù)據(jù)存儲到 Undo Log 中,當事務需要回滾時,MySQL 會根據(jù) Undo Log 中的記錄將數(shù)據(jù)還原為修改前的狀態(tài)。
具體的如下圖所示:
圖片
從圖中可知:
- 每一個 DML 語句都會為其生成對應的 Redo log 和 Undo log。
Redo log 記錄正向修改;
Undo log 記錄逆向恢復;
- 事務提交應用全部 Redolog 以持久化正向修改;
- 事務回滾應用全部 Undolog 以逆向恢復;
其中,可以看出存在兩個核心流程:
- 向前補償:redo log 記錄了事務執(zhí)行的過程,以及事務提交前的數(shù)據(jù)修改,可以通過重做日志來恢復數(shù)據(jù),實現(xiàn)向前補償;
- 向后補償:undo log 記錄了事務執(zhí)行過程中對數(shù)據(jù)的修改,可以用于回滾事務,實現(xiàn)向后補償;
除了兩種補償機制外,還涉及一個重要的組件“補償管理器”,用于對補償機制進行統(tǒng)一協(xié)調(diào)。
2.2. 2PC 和 XA
2PC(Two-Phase Commit)和XA是分布式事務中常用的協(xié)議和接口:
- 2PC是分布式事務協(xié)議,用于在分布式系統(tǒng)中協(xié)調(diào)多個參與者的事務提交或回滾。它包括兩個階段:準備階段和提交階段,參與者在準備階段告知協(xié)調(diào)者它們是否可以正常提交,如果都能正常提交,則在提交階段所有參與者都提交事務。如果有一個參與者無法正常提交,則所有參與者都需要回滾;
- XA是一組應用程序接口(API),它使應用程序能夠參與分布式事務,并與事務管理器協(xié)同工作,以保證事務的一致性。XA接口包括三個接口:XA Transactions、XA Resource、XA Resource Manager,用于實現(xiàn)分布式事務的協(xié)調(diào)和管理。
MySQL 采用了兩階段提交(Two-Phase Commit,簡稱 2PC)協(xié)議,保證 Redolog 和 Binlog 間的數(shù)據(jù)一致性,確保事務在所有相關節(jié)點(包括 Redolog 和 Binlog)執(zhí)行的情況下,要么全部提交成功,要么全部回滾失敗。
2PC只能應用于兩個事務參與者的場景,而XA可以應用于多個事務參與者的場景,具體如圖所示:
圖片
XA 定義了一組接口:
- XA資源管理器(XA Resource Manager,RM):用于管理分布式事務的資源,如數(shù)據(jù)庫、消息隊列等;
- XA事務管理器(XA Transaction Manager,TM):用于協(xié)調(diào)各個資源管理器的事務處理;
- XA接口:XA接口允許應用程序參與到分布式事務的協(xié)調(diào)中,包括開始、提交或回滾事務等操作;
對應的事務提交和回滾流程如下:
- 應用程序通過XA接口開始一個分布式事務,XA事務管理器為該事務分配一個唯一的全局事務ID;
- 應用程序使用XA接口將某些操作注冊為分布式事務的一部分,這些操作可以涉及多個XA資源管理器;
- 當應用程序執(zhí)行到提交事務的代碼時,XA事務管理器先協(xié)調(diào)各個XA資源管理器,檢查這些資源管理器是否都能夠提交事務;
- 如果所有的資源管理器都能夠提交事務,則XA事務管理器向各個資源管理器發(fā)送提交事務的請求,并等待它們的響應;
- 如果其中有任何一個資源管理器不能提交事務,則XA事務管理器向各個資源管理器發(fā)送回滾事務的請求,并等待它們的響應;
- 當所有的資源管理器都響應提交或回滾事務的請求后,XA事務管理器將事務的狀態(tài)(提交或回滾)通知給應用程序,并釋放資源。
2PC (包括升級后的 3PC),在事務執(zhí)行的整個流程中都需要對資源進行鎖定,在分布式環(huán)境下將大幅增加系統(tǒng)響應時間,降低整個系統(tǒng)的吞吐,在實際工作中使用的非常少。
2.3. TCC
TCC 是實現(xiàn)分布式事務解決方案的一種有效方法,更是真正應用于實際工作的一大解決方案。
圖片
TCC (try-confirm-cancel) 是一種分布式事務解決方案,它將一個分布式事務拆分成三個過程:
- Try 操作:嘗試執(zhí)行分布式事務中的操作,檢查所有參與方是否準備好執(zhí)行事務。如果準備好,則鎖定資源,等待確認或取消操作;
- Confirm 操作:確認執(zhí)行分布式事務中的操作,提交所有參與方的操作。如果有任何錯誤,則回滾所有操作并釋放鎖定的資源;
- Cancel 操作:取消執(zhí)行分布式事務中的操作,回滾所有參與方的操作并釋放鎖定的資源;
TCC 的操作流程如下:
- 應用程序向協(xié)調(diào)者請求分布式事務,并傳輸所有需要執(zhí)行的操作;
- 協(xié)調(diào)者根據(jù) TCC 的分布式事務處理策略創(chuàng)建一個唯一的分布式事務 ID,并將它分配給每個參與方;
- 各參與方執(zhí)行 Try 操作,并鎖定需要訪問的資源;
- 協(xié)調(diào)者檢查所有參與方是否準備好執(zhí)行操作,如果所有參與方都準備好,則進入 Confirm 階段;
- Confirm 階段中,各參與方確認執(zhí)行操作,并將結果提交給協(xié)調(diào)者;
- 如果有任何錯誤,協(xié)調(diào)者將回滾所有操作并釋放鎖定的資源。否則,所有參與方之間的事務將得到確認執(zhí)行,釋放資源并關閉事務;
- 如果任何參與方在 Try 階段失敗,則進入 Cancel 階段;
- Cancel 階段中,各參與方撤銷所有操作并釋放鎖定的資源;
- 協(xié)調(diào)者記錄每個階段的操作,以便處理異常情況;
TCC 是一種補償型事務機制,通過人工干預來處理異常,本身具備極佳的靈活性,適用于各種不同類型的應用場景。
2.4. 事務一致性本質
看了不少一致性解決方案,不知道有沒有發(fā)現(xiàn)一些規(guī)律?
核心組件基本一致:
- 應用程序:簡單理解為開發(fā)的應用系統(tǒng),借助事務管理器和資源管理的的能力,完成事務一致性保障;
- 事務管理器:事務的協(xié)調(diào)者,接收應用程序的請求,對多個資源管理器進行協(xié)調(diào),共同完成正向補償和逆向補償;
- 資源管理器:單一資源管理者,對外提供正向補償接口和逆向補充接口,供應用程序和事務管理器使用;
核心流程基本一致:
- 正向補償:應用流程向前推進,最終從一個狀態(tài)變化為另一個狀態(tài);
- 逆向補償:應用流程向后推進,將所有操作進行回滾,使其恢復到前一狀態(tài);
簡單來說:事務一致性就是通過協(xié)調(diào)各個參與節(jié)點來實現(xiàn)分布式事務的提交或回滾,確保所有涉及到的操作,要么全部執(zhí)行成功,要么全部不執(zhí)行。不同的實現(xiàn)方式只是不同的工具,其實現(xiàn)思路基本一致。
3. 業(yè)務一致性保障機制
前人已經(jīng)為我們提供足夠多的工具,如何更好的使用這些工具,就需要對業(yè)務場景進行深入分析。
業(yè)務系統(tǒng)一致性是指在多個系統(tǒng)或不同的環(huán)境中,不同用戶或系統(tǒng)操作所產(chǎn)生的數(shù)據(jù)在邏輯上是相同的。它的本質是確保在任何情況下,不同系統(tǒng)或用戶產(chǎn)生的數(shù)據(jù)都是一致的,并且在系統(tǒng)中的所有操作都是以預期方式進行的。業(yè)務系統(tǒng)一致性是確保數(shù)據(jù)的準確性和可靠性的關鍵因素,可以有效地避免數(shù)據(jù)錯誤和丟失,提高業(yè)務系統(tǒng)的可用性和可靠性,保障企業(yè)的持續(xù)發(fā)展。\\如下圖所示:
圖片
如果可重試性事務間不存在依賴關系,可以并行執(zhí)行,具體如下:
圖片
在一個復雜的業(yè)務流程中,可以將事務分為三類:
- 關鍵性事務:指的是系統(tǒng)中最為關鍵的一步操作,如果事務提交失敗,則進行回滾操作;如果事務提交成功,則成為事實,無法回滾;
- 可補償性事務:指的是在關鍵性事務之前的事務操作,通常提供正向和逆向兩組操作,正向操作失敗或關鍵事務失敗,在會逆序調(diào)用逆向接口,以對操作進行回滾;
- 可重試性事務:指的是關鍵事務之后的事務操作,關鍵事務提交成功,則事實已定,下游通過重試的方式完成事務;
我們以分布式系統(tǒng)中的下單流程為例:
圖片
- 關鍵性事務:就是下單操作,將用戶的信息保存到數(shù)據(jù)庫。保存失敗,對已經(jīng)操作的庫存和優(yōu)惠券進行逆向恢復;保存成功,通過重試保障下游事務的一致性;
- 可補償性事務:指的是優(yōu)惠券和庫存服務提供的正向和逆向操作,正向操作可以通過逆向操作進行恢復;
- 可重試性事務:指的是添加自動取消任務、保存操作日志、發(fā)送 MQ,當訂單數(shù)據(jù)保存成功后,這三者通過不斷重試保障最終都會執(zhí)行;
3.1. 關鍵性事務
關鍵性事務:指在分布式系統(tǒng)中,只有當某個事務被成功提交后,整個系統(tǒng)才能認為這個事務是成功的。如果這個事務失敗了,那么整個系統(tǒng)就會回滾到之前的狀態(tài)。例如支付、訂單提交等。
從關鍵性事務的使用場景出發(fā),最適合的工具便是關系數(shù)據(jù)庫的事務保障。
圖片
- 事務提交成功:整個流程向前補償,推動可重試性事務通過不斷重試最終完成業(yè)務邏輯;
- 事務提交失?。河|發(fā)整個流程回滾,逆序調(diào)用可補償事務的回滾接口恢復狀態(tài);
3.2. 可補償事務
可補償事務指在某些業(yè)務操作中,如果其中一些子操作執(zhí)行失敗,可以由后續(xù)補償操作進行補救,達到一定的業(yè)務目的,例如在資金交易中,如果賬戶余額不足而支付子操作失敗,可以通過撤銷訂單等補償操作來保障交易的正確性。
對于可補償事務,需要提供兩組操作:
- 正向:標準的業(yè)務操作,比如庫存鎖定
- 逆向:針對正向操作的恢復操作,比如釋放鎖定庫存
3.2.1. Seata
Seata 是一個開源的分布式事務解決方案,旨在解決分布式系統(tǒng)中的事務一致性問題。在傳統(tǒng)的分布式系統(tǒng)中,由于各個服務之間的數(shù)據(jù)交互和操作都是獨立進行的,因此很容易出現(xiàn)數(shù)據(jù)不一致的情況。這會導致系統(tǒng)出現(xiàn)各種異常情況,如數(shù)據(jù)丟失、重復提交等,從而影響系統(tǒng)的穩(wěn)定性和可靠性。
Seata 提供了多種解決方案來解決分布式事務一致性問題。其中包括 XA 模式、TCC 模式和 SAGA 模式等。
- XA 模式是一種基于數(shù)據(jù)庫的事務管理模式,Seata 通過與數(shù)據(jù)庫進行交互來實現(xiàn)分布式事務的一致性。該模式適用于對數(shù)據(jù)一致性要求比較高的業(yè)務場景,如金融、電商等。但是,由于需要與數(shù)據(jù)庫進行交互,因此該模式的性能相對較低;
- AT模式(基于應用層的兩階段提交方式):AT模式實現(xiàn)在應用程序中嵌入事務語義,通過協(xié)調(diào)維護必要的鎖,實現(xiàn)多個業(yè)務節(jié)點之間跨多個數(shù)據(jù)庫表的事務。適用于關系型數(shù)據(jù)庫的應用場景,如電商下單等。
- TCC 模式是一種基于補償?shù)氖聞展芾砟J?,Seata 通過預留資源、嘗試執(zhí)行、確認執(zhí)行和回滾執(zhí)行四個階段來實現(xiàn)分布式事務的一致性。該模式適用于對性能要求比較高的業(yè)務場景,如游戲、社交等。但是,由于需要進行多次異步通信,因此該模式的復雜度較高;
- SAGA 模式是一種基于事件驅動的事務管理模式,Seata 通過將一個大的分布式事務拆分成多個小的本地事務,并通過異步消息傳遞來實現(xiàn)分布式事務的一致性。該模式適用于對性能和可用性要求比較高的業(yè)務場景,如微服務架構下的系統(tǒng)。但是,由于需要進行多次異步通信和狀態(tài)管理,因此該模式的復雜度也較高。
Seata 還提供了一些重要的功能,如事務日志記錄、故障恢復、動態(tài)擴展等,使得用戶可以更加方便地使用該框架來解決分布式事務一致性問題。同時,Seata 還具有高性能、高可用性和易用性等特點,可以滿足各種不同場景下的需求。
【注】感興趣的話,可以找下 seata 的官方文檔。
3.2.2. Context + Rollback
Seata 雖好,但中間件的引入將大幅提升系統(tǒng)的復雜性,對于一些不太嚴謹?shù)膱鼍盎蛘咭恍┻\維能力不足的小團隊可以自己實現(xiàn)回滾方案。
整體方案如下:
圖片
- 創(chuàng)建一個 Context 對象,用于保存整個流程的上下文數(shù)據(jù)。其中存在一個 List<RollbackEntry> 屬性,維護待回滾任務列表;
- 每操作完一個正向流程,向 Context 中注冊一個逆向回調(diào),及 Rollback 任務;
- 如果
關鍵事務提交成功,Context 注冊的 RollbackEntry 便失去意義;
關鍵事務提交失敗,調(diào)用 Context 的 fireFallback 方法進行逆向補償,fireFallback 方法逆向調(diào)用注冊的回滾方法,從而恢復業(yè)務狀態(tài)
該方案基于內(nèi)存實現(xiàn),存在失靈的情況,不建議使用在嚴謹?shù)膱鼍啊?/p>
3.3. 可重試性事務
可重試型事務指在業(yè)務操作中,如果某些操作由于網(wǎng)絡波動等原因導致失敗,可以通過重新執(zhí)行這些操作來達到其預期的結果,例如在發(fā)送短信驗證碼時,由于網(wǎng)絡狀況不佳而發(fā)送失敗,可以重新嘗試發(fā)送,直到發(fā)送成功為止。
可重試性事務沒有失敗,只有成功,哪怕是短暫的失敗也會通過不限的重試使其最終達到成功狀態(tài)。
3.3.1. @Retry
@Retry 是 Spring 框架提供的一個注解,用于在方法調(diào)用失敗時自動進行重試。
通過 @Retry 注解,我們可以定義重試的次數(shù)、間隔時間和異常類型等信息,從而實現(xiàn)更可靠的方法調(diào)用。
具體來說,@Retry 注解可以通過以下屬性來配置:
- maxAttempts:最大重試次數(shù);
- value:重試間隔時間的數(shù)值表示;
- fixedDelay:是否固定等待重試間隔時間后再進行下一次嘗試;
- backoffPolicy:重試間隔時間的退避策略;
- allowCoreThreadTimeOut:是否允許在核心線程上進行超時等待;
- excludeExceptions:需要排除的異常類型;
- excludeClassNames:需要排除的類名列表;
- loggerMessage:日志輸出格式;
- fallbackMethodName:當所有重試都失敗后,執(zhí)行的方法名稱;
我們看下具體的使用:
- 基于計數(shù)器的重試實現(xiàn)
@Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void doSomething() throws Exception {
// 業(yè)務邏輯代碼
}
該實現(xiàn)會在方法調(diào)用失敗時進行最多3次的重試,每次重試之間會等待1秒的時間。如果超過3次重試仍然失敗,則拋出異常。
- 基于自定義異常處理的重試實現(xiàn)
@Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000), fallback = @Fallback(fallbackMethod = "doDefault"))
public void doSomething() throws Exception {
// 業(yè)務邏輯代碼
}
private String doDefault(Exception e) {
// 當出現(xiàn)指定異常時,執(zhí)行該方法進行重試處理
}
該實現(xiàn)會在方法調(diào)用失敗時進行最多3次的重試,每次重試之間會等待1秒的時間。如果超過3次重試仍然失敗,則會執(zhí)行 doDefault 方法來進行重試處理。在該方法中,我們可以自定義處理方式來處理異常情況。
@Retry 仍舊是一個內(nèi)存解決方案,在極端場景下可能出現(xiàn)任務丟失的情況。因此在實際工作中,很少用于可重試性事務這種場景。
3.3.2. MQ
MQ(消息隊列)消費者重試機制是指在消費消息時,如果消費者無法成功消費消息(比如網(wǎng)絡異常、服務器故障等原因),會自動重試一定次數(shù)或間隔一定時間后再次嘗試消費消息,以保證消息的可靠性和可用性。
如下圖所示:
im
具有MQ的可重試性事務,需要以下保障:
- 保障業(yè)務操作與消費發(fā)送之間的一致性:業(yè)務操作成功,消息必須發(fā)送成功;業(yè)務操作失敗,消息不能發(fā)送;
- 保障消息投遞和消費消費之間的一致性:對于消費失敗的消息,MQ 會自動進行重試,直至消費成功;
一般情況下會采用多次投遞的方式來實現(xiàn)消息投遞和消息消費之間的一致性,所以消息消費者需要保障冪等性,避免多次投遞造成的業(yè)務問題。
3.3.2.1. 半消息
RocketMQ事務消息是一種支持分布式事務的消息模型,將消息生產(chǎn)和消費與業(yè)務邏輯綁定在一起,確保消息發(fā)送和事務執(zhí)行的原子性,保證消息的可靠性。
事務消息分為兩個階段:發(fā)送消息和確認消息,確認消息分為提交和回滾兩個操作。在提交操作執(zhí)行完畢后,消息才會被消費端消費,而在回滾操作執(zhí)行完畢后,消息會被刪除,從而達到了事務的一致性和可靠性。
事務消息的發(fā)生流程如下:
圖片
- 生產(chǎn)者發(fā)送prepare消息到RocketMQ服務端,RocketMQ將消息存儲到本地并返回結果;
- 生產(chǎn)者開始執(zhí)行本地事務,并根據(jù)本地事務的結果將狀態(tài)信息提交給RocketMQ服務端;
- 如果本地事務執(zhí)行成功,生產(chǎn)者向RocketMQ服務端發(fā)送commit消息;
- 如果本地事務執(zhí)行失敗,生產(chǎn)者向RocketMQ服務端發(fā)送rollback消息;
- RocketMQ接收到commit或rollback消息后,對消息進行投放或刪除;
如果生成者發(fā)送 prepare 消息后,未在規(guī)定時間內(nèi)發(fā)送 commit 或 rollback 消息,RocketMQ 將進入恢復流程,具體如下:
圖片
- 如果在回查的時間之前沒有收到相應的 commit 或 rollback 消息,則 RocketMQ 會將對該 prepare 消息進行回查;
- 應用程序接收到回查指令,從業(yè)務庫中獲取數(shù)據(jù),并根據(jù)業(yè)務邏輯進行判斷,最終是 commit 還是 rollback;
- RocketMQ 接收到 commit 或 rollback 回復后,進行相應動作,從而實現(xiàn)業(yè)務操作和消息發(fā)送的一致性;
使用 RocketMQ 的事務消息代碼示例如下:
// 編寫事務監(jiān)聽器類
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
// 執(zhí)行本地事務
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
System.out.println("executeLocalTransaction " + value);
// TODO 執(zhí)行本地事務,并返回事務狀態(tài)
// 本例假定 index 為偶數(shù)的消息執(zhí)行成功,奇數(shù)的消息執(zhí)行失敗
if (value % 2 == 0) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 檢查本地事務狀態(tài)
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("checkLocalTransaction " + msg.getTransactionId());
// 模擬檢查本地事務狀態(tài),返回事務狀態(tài)
boolean committed = prepare(true);
if (committed) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
// 模擬操作預處理邏輯
private boolean prepare(boolean commit) {
System.out.println("prepare " + (commit ? "commit" : "rollback"));
return commit;
}
}
// 編寫發(fā)送消息的代碼
public class Producer {
private static final String NAME_SERVER_ADDR = "localhost:9876";
public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("MyGroup");
producer.setNamesrvAddr(NAME_SERVER_ADDR);
// 注冊事務監(jiān)聽器
producer.setTransactionListener(new TransactionListenerImpl());
producer.start();
// 發(fā)送事務消息
String[] tags = {"TagA", "TagB", "TagC"};
for (int i = 0; i < 3; i++) {
Message msg = new Message("TopicTest", tags[i], ("Hello RocketMQ " + i).getBytes(StandardCharsets.UTF_8));
// 在消息發(fā)送時傳遞給事務監(jiān)聽器的參數(shù)
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
}
// 關閉生產(chǎn)者
producer.shutdown();
}
}
單看代碼很難理解,簡單畫了張圖,具體如下:
圖片
其核心部分就是 TransactionListener 實現(xiàn),其他部分與正常的消息發(fā)送基本一致,TransactionListener 主要完成:
- 執(zhí)行本地事務,也就是業(yè)務操作;
- 執(zhí)行結果檢測,通過反查業(yè)務數(shù)據(jù),決定消息的后續(xù)處理策略;
為了使用事務消息,我們不得不在TransactionListener中編寫進行大量的適配邏輯,增加研發(fā)成本,同時由于邏輯被拆分到多處,也增加了代碼的理解成本。
事務消息存在一定的問題:
- 與 MQ 實現(xiàn)強相關,并不是每個 MQ 實現(xiàn)都對事務消息提供支持;
- API 比較晦澀,存在一定的學習成本,同時需要對業(yè)務邏輯拆分到 Listener 中,增加理解成本;
有沒有實用性強、使用簡單的方案,那可以使用 事務消息表 方案。
3.3.2.2. 事務消息表
事務消息表方案是一種常用的保證消息發(fā)送與業(yè)務操作一致性的方法。該方案基于數(shù)據(jù)庫事務和消息隊列,將消息發(fā)送和業(yè)務操作放入同一個事務中,并將業(yè)務操作和消息發(fā)送的狀態(tài)記錄在數(shù)據(jù)庫的消息表中,以實現(xiàn)消息的可靠性和冪等性。
如下圖所示:
圖片
核心流程如下:
- 應用程序開啟一個數(shù)據(jù)庫事務,并在事務中執(zhí)行業(yè)務操作和消息發(fā)送;
- 在事務中,將業(yè)務操作和消息發(fā)送的狀態(tài)記錄到消息表中;
- 如果業(yè)務操作執(zhí)行成功,并且消息發(fā)送成功,提交事務,否則回滾事務;
- 定時掃描消息表,并根據(jù)消息狀態(tài)重新發(fā)送未被確認的消息。如果消息發(fā)送成功,更新消息狀態(tài);否則根據(jù)重試次數(shù)更新消息狀態(tài)或者丟棄消息;
通過事務消息表方案,可以保證消息的可靠性和冪等性。即使在消息發(fā)送失敗或應用程序崩潰的情況下,也可以通過重新發(fā)送消息將業(yè)務操作和消息發(fā)送的狀態(tài)同步。同時,該方案可以避免消息重復發(fā)送和漏發(fā)的情況。
作為一種通用解決方案,lego 對其進行支持,可參考 reliable-message 模塊。
4. 業(yè)務補償
不管在設計時使用哪種方案,都是在盡力降低不一致出現(xiàn)的概率,但可怕的是不一致問題終究會發(fā)生。
是不是有些奇怪,做了這么多還是無法從根源上徹底解決一致性問題,在實際工作中就是這樣:
- 并不是所有的可補償事務都能回滾成功:在正向流程中我們都會對資源進行鎖定,如果其他操作破壞了鎖定資源或者破壞了準入條件,程序將無法正?;貪L,必須人工介入進行解決。比如,生單時成功鎖定優(yōu)惠券,但超管發(fā)現(xiàn)優(yōu)惠券發(fā)放錯誤對其進行回收,在進行優(yōu)惠券回滾時,由于優(yōu)惠券處于不可用狀態(tài),導致無法正常回滾;
- 并不是所有的可重試事務都能重試成功:業(yè)務執(zhí)行到可重試事務,只能證明其滿足關鍵事務之前的條件,并不一定滿足下游可重試事務的條件。比如,支付成功后需要給用戶發(fā)送微信消息,但用戶授權信息已經(jīng)過期導致消息無法發(fā)送;
- 業(yè)務迭代引入 bug 會破壞事務機制:這個就更常見了,由于bug導致流程錯誤,不得不修復問題和數(shù)據(jù)
除了主動降低不一致性概率,還需要添加一些被動保護機制,也就是常說的業(yè)務補償。
4.1. 查詢模式
查詢模型是最常用的一種方式,主要用于應對網(wǎng)絡傳輸中的第三態(tài)問題。
第三態(tài)指的是在分布式系統(tǒng)中,在進行跨網(wǎng)絡調(diào)用時,調(diào)用方無法確定被調(diào)用方的狀態(tài)是否改變了,因為這兩者之間存在一段未知而不可控的網(wǎng)絡延遲時間,導致調(diào)用方無法立即得到被調(diào)用方的結果。這種情況下,第三態(tài)可以看做是一個未知的狀態(tài),需要通過一些機制來解決這個問題。
圖片
當網(wǎng)絡調(diào)用出現(xiàn)第三態(tài)時,最簡單的方式便是對不確定的狀態(tài)進行查詢,如上圖所示:
- 調(diào)用方調(diào)用服務完成業(yè)務操作,如果成功拿到執(zhí)行結果,則直接進行后續(xù)流程;
- 如果發(fā)生網(wǎng)絡超時,將通過狀態(tài)查詢接口來檢查之前的操作是否完成,如果:
已完成,則繼續(xù)執(zhí)行后續(xù)流程;
未完成,在重新發(fā)起業(yè)務調(diào)用;
RocketMQ 的事務消息便是基于該機制進行實現(xiàn)。
4.2. 任務檢測模式
當一個業(yè)務操作完成后,需要處理多個后續(xù)任務,為了保障所有任務都會被執(zhí)行,可以使用該模式。
如下圖所示:
圖片
image
- 業(yè)務操作后,將業(yè)務變更和檢測任務在同一事務保護下進行入庫;
- 系統(tǒng)繼續(xù)執(zhí)行后續(xù)任務,執(zhí)行完成后對任務狀態(tài)進行更新;
- 系統(tǒng)周期性對超時未執(zhí)行的任務進行加載,并進行檢測,如果
已經(jīng)執(zhí)行,則更新任務狀態(tài)
如果未執(zhí)行,則觸發(fā)任務執(zhí)行
本地消息表就是基于該模式進行構建。
4.3. 對賬模式
對賬模式經(jīng)常出現(xiàn)在與銀行等金融機構對接的場景。
圖片
業(yè)務對賬思路非常簡單:
- 從不同的業(yè)務系統(tǒng)獲取對賬數(shù)據(jù);
- 按照規(guī)則進行雙向對賬,如果
一致,則說明系統(tǒng)一致
不一致,進行報警,人工介入進行處理
必須是雙向對賬,單向對賬會出現(xiàn)數(shù)據(jù)丟失情況。
5. 小結
一致性是分布式系統(tǒng)面臨的巨大挑戰(zhàn),根據(jù)不同場景可以將一致性分為:
- 事務一致性。在一個事務內(nèi)的所有操作,要么全部完成,要么全部不完成,即保證這些操作是對數(shù)據(jù)的一致更新,避免數(shù)據(jù)出現(xiàn)不一致的情況。主要通過使用事務保證來實現(xiàn),例如:關系型數(shù)據(jù)庫的ACID事務。
- 副本一致性。各個副本之間的數(shù)據(jù)保持一致。當數(shù)據(jù)發(fā)生變化時,需要將這個變化同步到所有的副本中。主要使用副本同步技術來實現(xiàn),例如MySQL的主從復制、MySQL 到 Redis的數(shù)據(jù)同步;
本文重點對事務一致性進行全方位的闡述,包括:
- 技術視角,常見的解決方案:
MySQL 實現(xiàn)
2PC和XA協(xié)議
TCC 解決方案
- 業(yè)務視角,將不同的事務進行分類,以便更好的解決:
- 關鍵事務
- 可補償性事務
- 可重試性事務
有了這些方案后,很多場景下仍需落地業(yè)務補充,常見方案包括:
- 查詢模型
- 任務檢查模式
- 對賬模型