一文講透微服務下如何保證事務的一致性

原文地址:梁桂釗的博客git

博客地址:http://blog.720ui.comgithub

歡迎關注公衆號:「服務端思惟」。一羣同頻者,一塊兒成長,一塊兒精進,打破認知的侷限性。算法

從本地事務到分佈式事務的演變

什麼是事務?回答這個問題以前,咱們先來看一個經典的場景:支付寶等交易平臺的轉帳。假設小明須要用支付寶給小紅轉帳 100000 元,此時,小明賬號會少 100000 元,而小紅賬號會多 100000 元。若是在轉帳過程當中系統崩潰了,小明賬號少 100000 元,而小紅賬號金額不變,就會出大問題,所以這個時候咱們就須要使用事務了。請參見圖 6-1。spring

這裏,體現了事務一個很重要的特性:原子性。事實上,事務有四個基本特性:原子性、一致性、隔離性、持久性。其中,原子性,即事務內的操做要麼所有成功,要麼所有失敗,不會在中間的某個環節結束。一致性,即便數據庫在一個事務執行以前和執行以後,數據庫都必須處於一致性狀態。若是事務執行失敗,那麼須要自動回滾到原始狀態,換句話說,事務一旦提交,其餘事務查看到的結果一致,事務一旦回滾,其餘事務也只能看到回滾前的狀態。隔離性,即在併發環境中,不一樣的事務同時修改相同的數據時,一個未完成事務不會影響另一個未完成事務。持久性,即事務一旦提交,其修改的數據將永久保存到數據庫中,其改變是永久性的。數據庫

本地事務經過 ACID 保證數據的強一致性。ACID是 Atomic(原子性)、Consistency(一致性)、 Isolation(隔離性)和 Durability(持久性)的縮寫 。在實際開發過程當中,咱們或多或少都有使用到本地事務。例如,MySQL 事務處理使用到 begin 開始一個事務,rollback 事務回滾,commit 事務確認。這裏,事務提交後,經過 redo log 記錄變動,經過 undo log 在失敗時進行回滾,保證事務的原子性。筆者補充下,使用 Java 語言的開發者都接觸過 Spring。Spring 使用 @Transactional 註解就能夠搞定事務功能。事實上,Spring 封裝了這些細節,在生成相關的 Bean 的時候,在須要注入相關的帶有 @Transactional 註解的 bean 時候用代理去注入,在代理中爲咱們開啓提交/回滾事務。請參見圖6-2。apache

隨着業務的高速發展,面對海量數據,例如,上千萬甚至上億的數據,查詢一次所花費的時間會變長,甚至會形成數據庫的單點壓力。所以,咱們就要考慮分庫與分表方案了。分庫與分表的目的在於,減少數據庫的單庫單表負擔,提升查詢性能,縮短查詢時間。這裏,咱們先來看下單庫拆分的場景。事實上,分表策略能夠概括爲垂直拆分和水平拆分。垂直拆分,把表的字段進行拆分,即一張字段比較多的表拆分爲多張表,這樣使得行數據變小。一方面,能夠減小客戶端程序和數據庫之間的網絡傳輸的字節數,由於生產環境共享同一個網絡帶寬,隨着併發查詢的增多,有可能形成帶寬瓶頸從而形成阻塞。另外一方面,一個數據塊能存放更多的數據,在查詢時就會減小 I/O 次數。水平拆分,把表的行進行拆分。由於表的行數超過幾百萬行時,就會變慢,這時能夠把一張的表的數據拆成多張表來存放。水平拆分,有許多策略,例如,取模分表,時間維度分表等。這種場景下,雖然咱們根據特定規則分表了,咱們仍然可使用本地事務。可是,庫內分表,僅僅是解決了單表數據過大的問題,但並無把單表的數據分散到不一樣的物理機上,所以並不能減輕 MySQL 服務器的壓力,仍然存在同一個物理機上的資源競爭和瓶頸,包括 CPU、內存、磁盤 IO、網絡帶寬等。對於分庫拆分的場景,它把一張表的數據劃分到不一樣的數據庫,多個數據庫的表結構同樣。此時,若是咱們根據必定規則將咱們須要使用事務的數據路由到相同的庫中,能夠經過本地事務保證其強一致性。可是,對於按照業務和功能劃分的垂直拆分,它將把業務數據分別放到不一樣的數據庫中。這裏,拆分後的系統就會遇到數據的一致性問題,由於咱們須要經過事務保證的數據分散在不一樣的數據庫中,而每一個數據庫只能保證本身的數據能夠知足 ACID 保證強一致性,可是在分佈式系統中,它們可能部署在不一樣的服務器上,只能經過網絡進行通訊,所以沒法準確的知道其餘數據庫中的事務執行狀況。請參見圖6-3。api

此外,不只僅在跨庫調用存在本地事務沒法解決的問題,隨着微服務的落地中,每一個服務都有本身的數據庫,而且數據庫是相互獨立且透明的。那若是服務 A 須要獲取服務 B 的數據,就存在跨服務調用,若是遇到服務宕機,或者網絡鏈接異常、同步調用超時等場景就會致使數據的不一致,這個也是一種分佈式場景下須要考慮數據一致性問題。請參見圖6-4。安全

總結一下,當業務量級擴大以後的分庫,以及微服務落地以後的業務服務化,都會產生分佈式數據不一致的問題。既然本地事務沒法知足需求,所以分佈式事務就要登上舞臺。什麼是分佈式事務?咱們能夠簡單地理解,它就是爲了保證不一樣數據庫的數據一致性的事務解決方案。這裏,咱們有必要先來了解下 CAP 原則和 BASE 理論。CAP 原則是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分區容錯性)的縮寫,它是分佈式系統中的平衡理論。在分佈式系統中,一致性要求全部節點每次讀操做都能保證獲取到最新數據;可用性要求不管任何故障產生後都能保證服務仍然可用;分區容錯性要求被分區的節點能夠正常對外提供服務。事實上,任何系統只可同時知足其中二個,沒法三者兼顧。對於分佈式系統而言,分區容錯性是一個最基本的要求。那麼,若是選擇了一致性和分區容錯性,放棄可用性,那麼網絡問題會致使系統不可用。若是選擇可用性和分區容錯性,放棄一致性,不一樣的節點之間的數據不能及時同步數據而致使數據的不一致。請參見圖 6-5。服務器

此時,BASE 理論針對一致性和可用性提出了一個方案,BASE 是 Basically Available(基本可用)、Soft-state(軟狀態)和 Eventually Consistent(最終一致性)的縮寫,它是最終一致性的理論支撐。簡單地理解,在分佈式系統中,容許損失部分可用性,而且不一樣節點進行數據同步的過程存在延時,可是在通過一段時間的修復後,最終可以達到數據的最終一致性。BASE 強調的是數據的最終一致性。相比於 ACID 而言,BASE 經過容許損失部分一致性來得到可用性。網絡

如今,業內比較經常使用的分佈式事務解決方案,包括強一致性的兩階段提交協議,三階段提交協議,以及最終一致性的可靠事件模式、補償模式,阿里的 TCC 模式。咱們會在後面的章節中詳細介紹與實戰。

強一致性解決方案

二階段提交協議

在分佈式系統中,每一個數據庫只能保證本身的數據能夠知足 ACID 保證強一致性,可是它們可能部署在不一樣的服務器上,只能經過網絡進行通訊,所以沒法準確的知道其餘數據庫中的事務執行狀況。所以,爲了解決多個節點之間的協調問題,就須要引入一個協調者負責控制全部節點的操做結果,要麼所有成功,要麼所有失敗。其中,XA 協議是一個分佈式事務協議,它有兩個角色:事務管理者和資源管理者。這裏,咱們能夠把事務管理者理解爲協調者,而資源管理者理解爲參與者。

XA 協議經過二階段提交協議保證強一致性。

二階段提交協議,顧名思義,它具備兩個階段:第一階段準備,第二階段提交。這裏,事務管理者(協調者)主要負責控制全部節點的操做結果,包括準備流程和提交流程。第一階段,事務管理者(協調者)向資源管理者(參與者)發起準備指令,詢問資源管理者(參與者)預提交是否成功。若是資源管理者(參與者)能夠完成,就會執行操做,並不提交,最後給出本身響應結果,是預提交成功仍是預提交失敗。第二階段,若是所有資源管理者(參與者)都回復預提交成功,資源管理者(參與者)正式提交命令。若是其中有一個資源管理者(參與者)回覆預提交失敗,則事務管理者(協調者)向全部的資源管理者(參與者)發起回滾命令。舉個案例,如今咱們有一個事務管理者(協調者),三個資源管理者(參與者),那麼這個事務中咱們須要保證這三個參與者在事務過程當中的數據的強一致性。首先,事務管理者(協調者)發起準備指令預判它們是否已經預提交成功了,若是所有回覆預提交成功,那麼事務管理者(協調者)正式發起提交命令執行數據的變動。請參見圖 6-6。

注意的是,雖然二階段提交協議爲保證強一致性提出了一套解決方案,可是仍然存在一些問題。其一,事務管理者(協調者)主要負責控制全部節點的操做結果,包括準備流程和提交流程,可是整個流程是同步的,因此事務管理者(協調者)必須等待每個資源管理者(參與者)返回操做結果後才能進行下一步操做。這樣就很是容易形成同步阻塞問題。其二,單點故障也是須要認真考慮的問題。事務管理者(協調者)和資源管理者(參與者)均可能出現宕機,若是資源管理者(參與者)出現故障則沒法響應而一直等待,事務管理者(協調者)出現故障則事務流程就失去了控制者,換句話說,就是整個流程會一直阻塞,甚至極端的狀況下,一部分資源管理者(參與者)數據執行提交,一部分沒有執行提交,也會出現數據不一致性。此時,讀者會提出疑問:這些問題應該都是小几率狀況,通常是不會產生的?是的,可是對於分佈式事務場景,咱們不只僅須要考慮正常邏輯流程,還須要關注小几率的異常場景,若是咱們對異常場景缺少處理方案,可能就會出現數據的不一致性,那麼後期靠人工干預處理,會是一個成本很是大的任務,此外,對於交易的核心鏈路也許就不是數據問題,而是更加嚴重的資損問題。

三階段提交協議

二階段提交協議諸多問題,所以三階段提交協議就要登上舞臺了。三階段提交協議是二階段提交協議的改良版本,它與二階段提交協議不一樣之處在於,引入了超時機制解決同步阻塞問題,此外加入了預備階段儘量提前發現沒法執行的資源管理者(參與者)而且終止事務,若是所有資源管理者(參與者)均可以完成,才發起第二階段的準備和第三階段的提交。不然,其中任何一個資源管理者(參與者)回覆執行,或者超時等待,那麼就終止事務。總結一下,三階段提交協議包括:第一階段預備,第二階段準備,第二階段提交。請參見圖 6-7。

三階段提交協議很好的解決了二階段提交協議帶來的問題,是一個很是有參考意義的解決方案。可是,極小機率的場景下可能會出現數據的不一致性。由於三階段提交協議引入了超時機制,若是出現資源管理者(參與者)超時場景會默認提交成功,可是若是其沒有成功執行,或者其餘資源管理者(參與者)出現回滾,那麼就會出現數據的不一致性。

最終一致性解決方案

TCC 模式

二階段提交協議和三階段提交協議很好的解決了分佈式事務的問題,可是在極端狀況下仍然存在數據的不一致性,此外它對系統的開銷會比較大,引入事務管理者(協調者)後,比較容易出現單點瓶頸,以及在業務規模不斷變大的狀況下,系統可伸縮性也會存在問題。注意的是,它是同步操做,所以引入事務後,直到全局事務結束才能釋放資源,性能多是一個很大的問題。所以,在高併發場景下不多使用。所以,阿里提出了另一種解決方案:TCC 模式。注意的是,不少讀者把二階段提交等同於二階段提交協議,這個是一個誤區,事實上,TCC 模式也是一種二階段提交。

TCC 模式將一個任務拆分三個操做:Try、Confirm、Cancel。假如,咱們有一個 func() 方法,那麼在 TCC 模式中,它就變成了 tryFunc()、confirmFunc()、cancelFunc() 三個方法。

tryFunc();
confirmFunc();
cancelFunc();

在 TCC 模式中,主業務服務負責發起流程,而從業務服務提供 TCC 模式的 Try、Confirm、Cancel 三個操做。其中,還有一個事務管理器的角色負責控制事務的一致性。例如,咱們如今有三個業務服務:交易服務,庫存服務,支付服務。用戶選商品,下訂單,緊接着選擇支付方式進行付款,而後這筆請求,交易服務會先調用庫存服務扣庫存,而後交易服務再調用支付服務進行相關的支付操做,而後支付服務會請求第三方支付平臺建立交易並扣款,這裏,交易服務就是主業務服務,而庫存服務和支付服務是從業務服務。請參見圖 6-8。

咱們再來梳理下,TCC 模式的流程。第一階段主業務服務調用所有的從業務服務的 Try 操做,而且事務管理器記錄操做日誌。第二階段,當所有從業務服務都成功時,再執行 Confirm 操做,不然會執行 Cancel 逆操做進行回滾。請參見圖 6-9。

如今,咱們針對 TCC 模式說說大體業務上的實現思路。首先,交易服務(主業務服務)會向事務管理器註冊並啓動事務。其實,事務管理器是一個概念上的全局事務管理機制,能夠是一個內嵌於主業務服務的業務邏輯,或者抽離出的一個 TCC 框架。事實上,它會生成全局事務 ID 用於記錄整個事務鏈路,而且實現了一套嵌套事務的處理邏輯。當主業務服務調用所有的從業務服務的 try 操做,事務管理器利用本地事務記錄相關事務日誌,這個案例中,它記錄了調用庫存服務的動做記錄,以及調用支付服務的動做記錄,並將其狀態設置成「預提交」狀態。這裏,調用從業務服務的 Try 操做就是核心的業務代碼。那麼, Try 操做怎麼和它相對應的 Confirm、Cancel 操做綁定呢?其實,咱們能夠編寫配置文件創建綁定關係,或者經過 Spring 的註解添加 confirm 和 cancel 兩個參數也是不錯的選擇。當所有從業務服務都成功時,由事務管理器經過 TCC 事務上下文切面執行 Confirm 操做,將其狀態設置成「成功」狀態,不然執行 Cancel 操做將其狀態設置成「預提交」狀態,而後進行重試。所以,TCC 模式經過補償的方式保證其最終一致性。

TCC 的實現框架有不少成熟的開源項目,例如 tcc-transaction 框架。(關於 tcc-transaction 框架的細節,能夠閱讀:https://github.com/changmingxie/tcc-transaction)tcc-transaction 框架主要涉及 tcc-transaction-core、tcc-transaction-api、tcc-transaction-spring 三個模塊。其中,tcc-transaction-core 是 tcc-transaction 的底層實現,tcc-transaction-api 是 tcc-transaction 使用的 API,tcc-transaction-spring 是 tcc-transaction 的 Spring 支持。 tcc-transaction 將每一個業務操做抽象成事務參與者,每一個事務能夠包含多個參與者。參與者須要聲明 try / confirm / cancel 三個類型的方法。這裏,咱們經過 @Compensable 註解標記在 try 方法上,並定義相應的 confirm / cancel 方法。

// try 方法
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

// confirm 方法
@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

// cancel 方法
@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

對於 tcc-transaction 框架的實現,咱們來了解一些核心思路。tcc-transaction 框架經過 @Compensable 切面進行攔截,能夠透明化對參與者 confirm / cancel 方法調用,從而實現 TCC 模式。這裏,tcc-transaction 有兩個攔截器,請參見圖 6-10。

  • org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可補償事務攔截器。

  • org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor,資源協調者攔截器。

這裏,須要特別關注 TransactionContext 事務上下文,由於咱們須要遠程調用服務的參與者時經過參數的形式傳遞事務給遠程參與者。在 tcc-transaction  中,一個事務org.mengyun.tcctransaction.Transaction能夠有多個參與者org.mengyun.tcctransaction.Participant 參與業務活動。其中,事務編號  TransactionXid  用於惟一標識一個事務,它使用 UUID 算法生成,保證惟一性。當參與者進行遠程調用時,遠程的分支事務的事務編號等於該參與者的事務編號。經過事務編號的關聯 TCC confirm / cancel 方法,使用參與者的事務編號和遠程的分支事務進行關聯,從而實現事務的提交和回滾。事務狀態 TransactionStatus 包含 : 嘗試中狀態 TRYING(1)、確認中狀態 CONFIRMING(2)、取消中狀態 CANCELLING(3)。此外,事務類型 TransactionType 包含 : 根事務 ROOT(1)、分支事務 BRANCH(2)。當調用 TransactionManager#begin() 發起根事務時,類型爲 MethodType.ROOT,而且事務 try 方法被調用。調用 TransactionManager#propagationNewBegin() 方法,傳播發起分支事務。該方法在調用方法類型爲 MethodType.PROVIDER 而且 事務 try 方法被調用。調用 TransactionManager#commit() 方法提交事務。該方法在事務處於 confirm / cancel 方法被調用。相似地,調用 TransactionManager#rollback() 方法,取消事務。請參見圖 6-11。

此外,對於事務恢復機制,tcc-transaction 框架基於 Quartz 實現調度,按照必定頻率對事務進行重試,直到事務完成或超過最大重試次數。若是單個事務超過最大重試次數時,tcc-transaction 框架再也不重試,此時須要手工介入解決。

這裏,咱們要特別注意操做的冪等性。冪等機制的核心是保證資源惟一性,例如重複提交或服務端的屢次重試只會產生一份結果。支付場景、退款場景,涉及金錢的交易不能出現屢次扣款等問題。事實上,查詢接口用於獲取資源,由於它只是查詢數據而不會影響到資源的變化,所以無論調用多少次接口,資源都不會改變,因此是它是冪等的。而新增接口是非冪等的,由於調用接口屢次,它都將會產生資源的變化。所以,咱們須要在出現重複提交時進行冪等處理。那麼,如何保證冪等機制呢?事實上,咱們有不少實現方案。其中,一種方案就是常見的建立惟一索引。在數據庫中針對咱們須要約束的資源字段建立惟一索引,能夠防止插入重複的數據。可是,遇到分庫分表的狀況是,惟一索引也就不那麼好使了,此時,咱們能夠先查詢一次數據庫,而後判斷是否約束的資源字段存在重複,沒有的重複時再進行插入操做。注意的是,爲了不併發場景,咱們能夠經過鎖機制,例如悲觀鎖與樂觀鎖保證數據的惟一性。這裏,分佈式鎖是一種常用的方案,它一般狀況下是一種悲觀鎖的實現。可是,不少人常常把悲觀鎖、樂觀鎖、分佈式鎖看成冪等機制的解決方案,這個是不正確的。除此以外,咱們還能夠引入狀態機,經過狀態機進行狀態的約束以及狀態跳轉,確保同一個業務的流程化執行,從而實現數據冪等。

補償模式

上節,咱們提到了重試機制。事實上,它也是一種最終一致性的解決方案:咱們須要經過最大努力不斷重試,保證數據庫的操做最終必定能夠保證數據一致性,若是最終屢次重試失敗能夠根據相關日誌並主動通知開發人員進行手工介入。注意的是,被調用方須要保證其冪等性。重試機制能夠是同步機制,例如主業務服務調用超時或者非異常的調用失敗須要及時從新發起業務調用。重試機制能夠大體分爲固定次數的重試策略與固定時間的重試策略。除此以外,咱們還能夠藉助消息隊列和定時任務機制。消息隊列的重試機制,即消息消費失敗則進行從新投遞,這樣就能夠避免消息沒有被消費而被丟棄,例如 RocketMQ 能夠默認容許每條消息最多重試 16 次,每次重試的間隔時間能夠進行設置。定時任務的重試機制,咱們能夠建立一張任務執行表,並增長一個「重試次數」字段。這種設計方案中,咱們能夠在定時調用時,獲取這個任務是不是執行失敗的狀態而且沒有超太重試次數,若是是則進行失敗重試。可是,當出現執行失敗的狀態而且超太重試次數時,就說明這個任務永久失敗了,須要開發人員進行手工介入與排查問題。

除了重試機制以外,也能夠在每次更新的時候進行修復。例如,對於社交互動的點贊數、收藏數、評論數等計數場景,也許由於網絡抖動或者相關服務不可用,致使某段時間內的數據不一致,咱們就能夠在每次更新的時候進行修復,保證系統通過一段較短的時間的自我恢復和修正,數據最終達到一致。須要注意的是,使用這種解決方案的狀況下,若是某條數據出現不一致性,可是又沒有再次更新修復,那麼其永遠都會是異常數據。

定時校對也是一種很是重要的解決手段,它採起週期性的進行校驗操做來保證。關於定時任務框架的選型上,業內比較經常使用的有單機場景下的 Quartz,以及分佈式場景下 Elastic-Job、XXL-JOB、SchedulerX 等分佈式定時任務中間件。關於定時校對能夠分爲兩種場景,一種是未完成的定時重試,例如咱們利用定時任務掃描還未完成的調用任務,並經過補償機制來修復,實現數據最終達到一致。另外一種是定時覈對,它須要主業務服務提供相關查詢接口給從業務服務覈對查詢,用於恢復丟失的業務數據。如今,咱們來試想一下電商場景的退款業務。在這個退款業務中會存在一個退款基礎服務和自動化退款服務。此時,自動化退款服務在退款基礎服務的基礎上實現退款能力的加強,實現基於多規則的自動化退款,而且經過消息隊列接收到退款基礎服務推送的退款快照信息。可是,因爲退款基礎服務發送消息丟失或者消息隊列在屢次失敗重試後的主動丟棄,都頗有可能形成數據的不一致性。所以,咱們經過定時從退款基礎服務查詢覈對,恢復丟失的業務數據就顯得特別重要了。

可靠事件模式

在分佈式系統中,消息隊列在服務端的架構中的地位很是重要,主要解決異步處理、系統解耦、流量削峯等場景。多個系統之間若是同步通訊很容易形成阻塞,同時會將這些系統會耦合在一塊兒。所以,引入了消息隊列,一方面解決了同步通訊機制形成的阻塞,另外一方面經過消息隊列進行業務解耦。請參見圖 6-12。

可靠事件模式,經過引入可靠的消息隊列,只要保證當前的可靠事件投遞而且消息隊列確保事件傳遞至少一次,那麼訂閱這個事件的消費者保證事件可以在本身的業務內被消費便可。這裏,請讀者思考,是否只要引入了消息隊列就能夠解決問題了呢?事實上,只是引入消息隊列並不能保證其最終的一致性,由於分佈式部署環境下都是基於網絡進行通訊,而網絡通訊過程當中,上下游可能由於各類緣由而致使消息丟失。

其一,主業務服務發送消息時可能由於消息隊列沒法使用而發生失敗。對於這種狀況,咱們可讓主業務服務(生產者)發送消息,再進行業務調用來確保。通常的作法是,主業務服務將要發送的消息持久化到本地數據庫,設置標誌狀態爲「待發送」狀態,而後把消息發送給消息隊列,消息隊列收到消息後,也把消息持久化到其存儲服務中,但並非當即向從業務服務(消費者)投遞消息,而是先向主業務服務(生產者)返回消息隊列的響應結果,而後主業務服務判斷響應結果執行以後的業務處理。若是響應失敗,則放棄以後的業務處理,設置本地的持久化消息標誌狀態爲「結束」狀態。不然,執行後續的業務處理,設置本地的持久化消息標誌狀態爲「已發送」狀態。

public void doServer(){
    // 發送消息
    send();
    // 執行業務
    exec();
    // 更新消息狀態
    updateMsg();
}

此外,消息隊列發生消息後,也可能從業務服務(消費者)宕機而沒法消費。絕大多數消息中間件對於這種狀況,例如 RabbitMQ、RocketMQ 等引入了 ACK 機制。注意的是,默認的狀況下,採用自動應答,這種方式中消息隊列會發送消息後當即從消息隊列中刪除該消息。因此,爲了確保消息的可靠投遞,咱們經過手動 ACK 方式,若是從業務服務(消費者)因宕機等緣由沒有發送 ACK,消息隊列會將消息從新發送,保證消息的可靠性。從業務服務處理完相關業務後經過手動 ACK 通知消息隊列,消息隊列才從消息隊列中刪除該持久化消息。那麼,消息隊列若是一直重試失敗而沒法投遞,就會出現消息主動丟棄的狀況,咱們須要如何解決呢?聰明的讀者可能已經發現,咱們在上個步驟中,主業務服務已經將要發送的消息持久化到本地數據庫。所以,從業務服務消費成功後,它也會向消息隊列發送一個通知消息,此時它是一個消息的生產者。主業務服務(消費者)接收到消息後,最終把本地的持久化消息標誌狀態爲「完成」狀態。說到這裏,讀者應該能夠理解到咱們使用「正反向消息機制」確保了消息隊列可靠事件投遞。固然,補償機制也是必不可少的。定時任務會從數據庫掃描在必定時間內未完成的消息並從新投遞。請參見圖 6-13。

注意的是,由於從業務服務可能收到消息處理超時或者服務宕機,以及網絡等緣由致使而消息隊列收不到消息的處理結果,所以可靠事件投遞而且消息隊列確保事件傳遞至少一次。這裏,從業務服務(消費者)須要保證冪等性。若是從業務服務(消費者)沒有保證接口的冪等性,將會致使重複提交等異常場景。此外,咱們也能夠獨立消息服務,將消息服務獨立部署,根據不一樣的業務場景共用該消息服務,下降重複開發服務的成本。

瞭解了「可靠事件模式」的方法論後,如今咱們來看一個真實的案例來加深理解。首先,當用戶發起退款後,自動化退款服務會收到一個退款的事件消息,此時,若是這筆退款符合自動化退款策略的話,自動化退款服務會先寫入本地數據庫持久化這筆退款快照,緊接着,發送一條執行退款的消息投遞到給消息隊列,消息隊列接受到消息後返回響應成功結果,那麼自動化退款服務就能夠執行後續的業務邏輯。與此同時,消息隊列異步地把消息投遞給退款基礎服務,而後退款基礎服務執行本身業務相關的邏輯,執行失敗與否由退款基礎服務自我保證,若是執行成功則發送一條執行退款成功消息投遞到給消息隊列。最後,定時任務會從數據庫掃描在必定時間內未完成的消息並從新投遞。這裏,須要注意的是,自動化退款服務持久化的退款快照能夠理解爲須要確保投遞成功的消息,由「正反向消息機制」和「定時任務」確保其成功投遞。此外,真正的退款出帳邏輯在退款基礎服務來保證,所以它要保證冪等性,及出帳邏輯的收斂。當出現執行失敗的狀態而且超太重試次數時,就說明這個任務永久失敗了,須要開發人員進行手工介入與排查問題。請參見圖 6-14。

總結一下,引入了消息隊列並不能保證可靠事件投遞,換句話說,因爲網絡等各類緣由而致使消息丟失不能保證其最終的一致性,所以,咱們須要經過「正反向消息機制」確保了消息隊列可靠事件投遞,而且使用補償機制儘量在必定時間內未完成的消息並從新投遞。

開源項目的分佈式事務實現解讀

開源項目中對分佈式事務的應用有不少值得咱們學習與借鑑的地方。本節,咱們就來對其實現進行解讀。

RocketMQ

Apache RocketMQ 是阿里開源的一款高性能、高吞吐量的分佈式消息中間件。在歷年雙 11 中,RocketMQ 都承擔了阿里巴巴生產系統所有的消息流轉,在覈心交易鏈路有着穩定和出色的表現,是承載交易峯值的核心基礎產品之一。RocketMQ 同時存在商用版 MQ 可在阿里雲上購買(https://www.aliyun.com/product/ons),阿里巴巴對於開源版本和商業版本,主要區別在於:會開源分佈式消息全部核心的特性,而在商業層面,尤爲是雲平臺的搭建上面,將運維管控、安全受權、深度培訓等歸入商業重中之重。

Apache RocketMQ 4.3 版本正式支持分佈式事務消息。RocketMQ 事務消息設計主要解決了生產者端的消息發送與本地事務執行的原子性問題,換句話說,若是本地事務執行不成功,則不會進行 MQ 消息推送。那麼,聰明的你可能就會存在疑問:咱們能夠先執行本地事務,執行成功了再發送 MQ 消息,這樣不就能夠保證事務性的?可是,請你再認真的思考下,若是 MQ 消息發送不成功怎麼辦呢?事實上,RocketMQ 對此提供一個很好的思路和解決方案。 RocketMQ 首先會發送預執行消息到 MQ,而且在發送預執行消息成功後執行本地事務。緊接着,它根據本地事務執行結果進行後續執行邏輯,若是本地事務執行結果是 commit,那麼正式投遞 MQ 消息,若是本地事務執行結果是 rollback,則 MQ 刪除以前投遞的預執行消息,不進行投遞下發。注意的是,對於異常狀況,例如執行本地事務過程當中,服務器宕機或者超時,RocketMQ 將會不停的詢問其同組的其餘生產者端來獲取狀態。請參見圖 6-15。

至此,咱們已經瞭解了 RocketMQ 的實現思路,若是對源碼實現感興趣的讀者,能夠閱讀org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction

ServiceComb

ServiceComb 基於華爲內部的 CSE(Cloud Service Engine) 框架開源而來,它提供了一套包含代碼框架生成,服務註冊發現,負載均衡,服務可靠性(容錯熔斷,限流降級,調用鏈追蹤)等功能的微服務框架。其中,ServiceComb Saga 是一個微服務應用的數據最終一致性解決方案。

Saga 拆分分佈式事務爲多個本地事務,而後由 Saga 引擎負責協調。若是整個流程正常結束,那麼業務成功完成;若是在這過程當中實現出現部分失敗,那麼Saga 引擎調用補償操做。Saga 有兩種恢復的策略 :向前恢復和向後恢復。其中,向前恢復對失敗的節點採起最大努力不斷重試,保證數據庫的操做最終必定能夠保證數據一致性,若是最終屢次重試失敗能夠根據相關日誌並主動通知開發人員進行手工介入。向後恢復對以前全部成功的節點執行回滾的事務操做,這樣保證數據達到一致的效果。

Saga 與 TCC 不一樣之處在於,Saga 比 TCC 少了一個 Try 操做。所以,Saga 會直接提交到數據庫,而後出現失敗的時候,進行補償操做。Saga 的設計可能致使在極端場景下的補償動做比較麻煩,可是對於簡單的業務邏輯侵入性更低,更輕量級,而且減小了通訊次數,請參見圖 6-16。

ServiceComb Saga 在其理論基礎上進行了擴展,它包含兩個組件: alpha 和 omega。alpha 充當協調者,主要負責對事務的事件進行持久化存儲以及協調子事務的狀態,使其得以最終與全局事務的狀態保持一致。omega 是微服務中內嵌的一個 agent,負責對網絡請求進行攔截並向 alpha 上報事務事件,並在異常狀況下根據 alpha 下發的指令執行相應的補償操做。在預處理階段,alpha 會記錄事務開始的事件;在後處理階段,alpha 會記錄事務結束的事件。所以,每一個成功的子事務都有一一對應的開始及結束事件。在服務生產方,omega 會攔截請求中事務相關的 id 來提取事務的上下文。在服務消費方,omega 會在請求中注入事務相關的 id來傳遞事務的上下文。經過服務提供方和服務消費方的這種協做處理,子事務能鏈接起來造成一個完整的全局事務。注意的是,Saga 要求相關的子事務提供事務處理方法,而且提供補償函數。這裏,添加 @EnableOmega 的註解來初始化 omega 的配置並與 alpha 創建鏈接。在全局事務的起點添加 @SagaStart 的註解,在子事務添加 @Compensable 的註解指明其對應的補償方法。 使用案例:https://github.com/apache/servicecomb-saga/tree/master/saga-demo

@EnableOmega
public class Application{
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

@SagaStart
public void xxx() { }


@Compensable
public void transfer() { }

如今,咱們來看一下它的業務流程圖,請參見圖 6-17。

更多精彩文章,盡在「服務端思惟」!

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索