天天看點

Spring Cloud Alibaba 分布式事務

  1. 簡介

    分布式一緻性是分布式系統亟需解決的關鍵問題之一,根據過去一年的調查問卷,在微服務的實踐中分布式事務是使用者遇到的最大痛點。目前市面缺少經過洪荒流量驗證的分布式事務元件,Seata 在阿裡經濟體内部經過了漫長的孵化,承載了雙11洪荒流量,實踐證明 Seata 是一款解決分布式資料一緻性的的優秀元件。Seata 于 2019 年正式對外開源,開源後就受到了大家的熱情追捧,一度蟬聯 GitHub 活躍排名榜首。Seata 除了提供了獨創的 AT 事務模式外,還擴充了 TCC、Saga 和 XA 事務模式,滿足大家對于不同業務場景中的需求。相關詳細資訊可參考其官網 Seata官網

  2. 學習目标

    了解分布式事務在業務中的核心使用場景和常用解決方案

    了解 Seata AT 事務模式的核心原理

    掌握 Seata 作為分布式事務元件與 Spring Cloud 的整合

    如何擴充一個 RPC 架構

    Seata 實戰

  3. 為什麼需要分布式事務?

    分布式事務不是在新架構下産生的新問題,即使在單體應用中同樣存在着分布式事務問題,典型的場景是單體應用執行方法中含有多個資料源。X/OPEN 對于這一問題,提出了含有三種角色的 DTP(Distributed Transaction Processing)模型并形成了 XA 規範來解決此問題。各廠商針對 XA 規範做了具體的實作,也就是大家常說的 XA協定。在 Java 體系中基于 DTP 模型提出了 JTA規範(參考 JSR 907), 定義了分布式事務中的事務管理器™與資料總管(RM)、應用程式(AP)等的 Java 接口。在Java EE時代,應用伺服器如weblogic 充當了 TM 的角色,而傳統關系資料庫通過實作 XA 協定充當了 RM 的角色。

随着網際網路的高速發展,龐大的使用者群體和快速的需求變化已經成為了傳統架構的痛點。在這種情況下,如何從系統架構的角度出發,建構出靈活、易擴充的系統來快速響應需求的變化,同時,随着使用者量的增加,如何保證系統的穩定性、高可用性、可伸縮性等等,成為了系統架構面臨的挑戰。微服務基于此背景應運而生,微服務架構越來越來越成為一種架構趨勢,其本質是分布式去中心化。但微服務架構絕不是銀彈,它不一定是一種能支撐未來一二十年的架構,引入微服務架構時需要我們根據業務場景,系統複雜性和團隊規模有步驟的進行。微服務架構的引入使分布式資料一緻性問題更為突出,由原來的單體應用拆分出來幾十甚至上百個微服務,如何保證服務間的一緻性?當在一條較長的微服務調用鍊中,位于中間位置的微服務節點出現異常,如何保證整個服務的資料一緻性?

分布式一緻性的引入,一定不可避免帶來性能問題,如何更高效的解決分布式一緻性問題,一直是我們緻力于解決此問題的關鍵出發點。在“一切都正常”的情況下,我們可以認為我們并不需要分布式事務。但系統很難滿足這種理想狀态,系統可能因為一個非法的參數校驗無法将服務鍊路繼續向下調用下去,系統可能出現令人反感的逾時問題,我們不清楚被調用的服務是否真正的執行了,被調用服務可能正在部署,網絡抖動亦或者節點當機導緻接口無法繼續調用。這些問題普遍存在于我們的系統中,業務的本質展現在資料上,資料不一緻的直接後果是可能産生資損,更嚴重的是如果不一緻的資料不能被及時發現,業務再次基于此資料的進行相關邏輯操作,會進一步導緻資料錯上加錯,最終很難溯源。

  1. 常見的分布式事務解決方案

    從是否滿足事務 ACID 特性上,我們可以将事務分為兩大類:剛性事務和柔性事務。在常見解決方案中XA事務屬于剛性事務解決方案,而其他的大多數解決方案如 TCC、Saga、消息最終一緻性則屬于柔性事務解決方案。以下将對幾種常見的事務方案做簡要的介紹:

消息最終一緻性

消息最終一緻性方案是在Seata問世之前,市面上應用最廣泛的一種解決方案。它本身具有削峰填谷,可異步化的優點,更多的适應于可異步化的末端鍊路消息通知場景。但是它本身也存在着一些缺點:需要依賴可靠消息元件,消息的可靠性很重要,大多數的原生消息元件故障時很難降級;實時性比較差,要經過多次網絡IO開銷和持久化,遇到隊列積壓情形實時性不可控;無法保證隔離性,在已發送消息和消息消費之前,中間資料對外可見,無法滿足事務 isolate 特性;隻能向前重試不可向後復原,消息消費無法成功時無法復原消息生産側的資料;無法保證多條消息間的資料一緻性。

XA

XA 标準提出後的20多年間未能得到持續的演進,在學術界有協定優化和日志協同處理等相關的研究,在工業界使用XA落地方案的相對較少,主要集中在應用伺服器的場景。XA方案要求相關的廠商提供其具體協定的實作,目前大部分關系資料庫支援了XA協定,但是支援程度不盡相同,例如,MySQL 在5.7 才對 xa_prepare 語義做了完整支援。XA 方案被人诟病的是其性能,其實更為嚴重的是對于連接配接資源的占用,導緻在高并發未有足夠的連接配接資源來響應請求成為系統的瓶頸。在微服務架構下 XA 事務方案随着微服務鍊路的擴充成為一種反伸縮模式,進一步加劇了資源的占用。另外 XA 事務方案要求事務鍊路中的resource全部實作XA協定方可使用,若其中某一資源不滿足,那麼就無法保證整個鍊路的資料一緻性。

TCC

TCC 方案要求使用者根據業務場景實作 try,confirm,cancel三個接口,由架構根據事務所處的事務階段和決議來自動調用使用者實作的三個接口。從概念上TCC架構可以認為是一種萬能架構,但是其難點是業務對于這三個接口的實作,開發成本相對較高,有較多業務難以做資源預留相關的邏輯處理,以及是否需要在預留資源的同時從業務層面來保證隔離性。是以,這種模式比較适應于金融場景中易于做資源預留的扣減模型。

Saga

有了 TCC 解決方案為什麼還需要 Saga 事務解決方案?上文提到了 TCC 方案中對業務的改造成本較大,對于内部系統可以自上而下大刀闊斧的推進系統的改造,但對于第三方的接口的調用往往很難推動第三方進行 TCC 的改造,讓對方為了你這一個使用者去改造 TCC 方案而其他使用者并不需要,需求上明顯也是不合理的。要求第三方業務接口提供正反接口比如扣款和退款,在異常場景下必要的資料沖正是合理的。另外,Saga 方案更加适應于工作流式的長事務方案并且可異步化。

上面提到了4種常用的分布式事務解決方案,Seata 內建了TCC、Saga 和 XA 方案。另外,Seata 還提供了獨創的 AT 強一緻分布式事務解決方案。下文将對 AT 方案進行簡要的介紹。

  1. AT事務模式
    Spring Cloud Alibaba 分布式事務

一個分布式事務有全局唯一的xid,由若幹個分支事務構成,每個分支事務有全局唯一的branchId。上圖展示了在一個分支事務中RM 與 TC 的互動過程。其中主要包含的互動動作如下:

  • branchRegister

    分布式事務一階段執行,分支事務在commit 之前與 TC 互動擷取 全局鎖 和傳回 branchId。全局鎖為Seata 應用鎖等同于修改資料記錄的行鎖,若擷取鎖失敗将會進行鎖重試,此處提供了兩種重試政策是否持有資料庫連接配接重試全局鎖,預設為釋放資料庫連接配接。若成功,則搶占全局鎖并傳回branchId,若重試到最大次數失敗,則發起全局事務的復原,對已完成的分支事務執行復原。

  • branchReport

    分布式事務一階段執行,本地事務commit 之後與 TC 互動,上報本地事務已完成辨別。目前 branchReport 動作已經在 1.0

    版本做了相關的優化,本地事務commit 不上報,本地事務rollback 上報。經過優化分布式事務的整體性能在globalCommit

    場景下最低提升25%,最高提升50%。本地事務rollback 上報可以幫助 TC 快速決策需要復原的分支事務。

  • branchCommit 分布式事務二階段執行,在形成globalCommit

    決議後執行。AT模式中此步驟異步執行來提升其性能,可以認為分布式事務globalCommit決議送出到TC

    釋放完全局鎖就已經完成了整個分布式事務的處理。branchCommit

    在AT模式主要用于删除一階段的undo_log,TC下發到RM後并不是立即執行,而是通過定時任務+sql 批量合并的方式來提升其處理性能。

  • branchRollback 分布式事務二階段執行,在形成globalRollback 決議後執行。RM 收到

    branchRollback 請求,取undo_log 表中對應的branchId 記錄解析rollback_info

    字段,對現有資料和undo log後鏡像對比,對比不同則分支事務復原失敗。對比成功則根據前鏡像構造sql并執行反向操作和删除undo

    log。

詳細處理過程和原理,可參考官網文檔關于AT模式的介紹:https://seata.io/zh-cn/docs/dev/mode/at-mode.html

  1. Seata 與 Spring Cloud 內建
    Spring Cloud Alibaba 分布式事務
    Spring Cloud Alibaba 分布式事務

如上圖,Seata 與 Spring Cloud Alibaba 內建代碼結構如上圖所示。從代碼上可以分為三大部分:rest、feign 和 web。AutoConfiguration 結尾的類是 @Configuration 類被 spring.factories 加載,負責建立 package 中所屬的 bean。

rest

對應restTemplate調用,實作 ClientHttpRequestInterceptor 接口,将目前事務上下文包裝到HttpRequest header中,加入到攔截器清單中。

feign

對應openFeign調用,這部分實作了事務上下文傳遞,與 Hystrix、Sentinel 、Ribbon 元件內建功能。需要特别注意是Hystrix中跨線程的事務上下文傳遞。這部分代碼大量使用了Builder、Wrapper模式,有興趣的同學可深入閱讀。

web

對應Spring Web Servlet中的處理,實作了 HandlerInterceptor接口。在 preHandle 預進行中取 httpRequest header中 Seata的事務上下文并使用 API 綁定到目前線程的事務處理上下文中,這種後續的資料源操作就自動加入到了分布式事務的鍊路中;在 afterCompletion 中做了目前線程事務上下文的清除,防止事務上下文線上程中污染。

  1. 如何擴充一個RPC架構?

    在上一章節,我們講到了Seata 是如何與 Spring Cloud 相內建的。Seata 目前已經內建了 Spring Cloud、Alibaba Dubbo、Apache Dubbo、Motan、gRPC 和 sofa-RPC 等微服務調用。結合上一節與Spring Cloud 的內建,擴充一個RPC 架構我們需要做哪些工作呢 ?主要可以分為兩大部分:

事務上下文傳遞

Seata 的事務上下文目前主要包含:xid和調用服務的分支事務類型。xid 為一個分布式事務的全局唯一辨別,類似于Tracing中的 traceId,隻有将 xid 傳遞下去才可能加入到分布式事務的鍊路中。調用服務的分支事務類型主要用于非 AT模式,例如在一個 TCC 分支事務中不能再嵌套 AT 分支事務。主流的RPC架構大多是基于TCP協定之上的私有協定封裝或者是基于HTTP協定。Seata 的事務上下文辨別都是簡單的字元串,序列化由RPC架構直接完成,大多數RPC 架構實作了 filter 或者 interceptor 接口,通過将Seata的事務上下文填充到協定中的attachment字段或者http header中,就可以簡單的完成事務上下文的傳遞。

事務上下文綁定和清除

在 RPC 的 provider 端收到 consumer 的請求,将事務上下文取出,通過 API 綁定到目前的執行線程中,這樣後續的業務處理都納入到了Seata 的分布式事務鍊路中。執行完業務處理後,需要對綁定到目前線程的事務上下文清除掉,防止産生線程事務上下文污染。

具體 API 可參考官網文檔:https://seata.io/zh-cn/docs/user/api.html

請大家思考一下如何去與Dubbo 內建的呢?https://github.com/seata/seata/tree/develop/integration/dubbo

  1. Seata 實戰

    本章節将通過一個基于Spring Cloud的訂單和庫存服務進行Seata 實戰。

    我們在沙箱環境裡準備好了一套seata的實際應用案例,服務調用結構如下所示:

    Spring Cloud Alibaba 分布式事務

如圖所示,服務鍊路中包含三個微服務:business(微服務入口)、order(訂單服務)和 storage(庫存服務)。

業務邏輯

通過url 調用 business 服務,business 服務會通過 openFeign 的方式分别調用 storage 和 order 服務。每個服務調用後會根據正常(ok)或異常(fail)的傳回值決定是否抛出異常,若傳回結果不為 ok 那麼 business 将抛出異常,觸發整個事務復原,預期資料至 business 方法執行前的資料并且庫存總量和與初始值一緻。當order 和 storage 兩個服務調用正常,使用者可以根據 url 中的 mockException=true 或 false 來注入一個 mock 異常,當注入異常後,期望資料復原至 business 方法執行前的資料并且庫存總量和與初始值一緻。

異常模拟

  1. 在 bussiness 中請求超過現有庫存,通過參數 count 來指定要扣減的庫存數量,當超出庫存,将抛出異常進行事務復原。
  2. 在 bussiness 中請求中加入 mockException=true 參數,觸發事務復原。

啟動步驟

以下請求 url 中的 localhost 請根據實際值進行替換。

  1. 啟動order 和 storage 服務,最後啟動 business 服務。服務的注冊和發現,Seata服務注冊和發現 在這裡使用了 Nacos 啟動成功後,可以通過Nacos 控制台的sandbox-seata namespace 看到如下服務:
    Spring Cloud Alibaba 分布式事務

通路 business 的url : http://localhost:8080/seata/check 來檢查初始化資料

Spring Cloud Alibaba 分布式事務

通過通路 business 的url:http://localhost:8080/seata/feign?count=5&mockException=true 來觸發業務邏輯:

Spring Cloud Alibaba 分布式事務

其中count 代表扣減庫存的數量,當庫存不足将觸發事務復原;mockException代表是否觸發業務異常,設定為true 會觸發業務異常,并通過分布式事務實作復原。

每次操作完business 請求後,通路 business 的url : http://localhost:8080/seata/check 來檢查操作後資料。

重複以上3(根據實際需要請求,不必每次請求相同),4步驟,最後再次通過通路 business 的url : http://localhost:8080/seata/check 來檢查結果資料:

Spring Cloud Alibaba 分布式事務

使用總結

  1. 引入依賴。com.alibaba.cloud:spring-cloud-starter-alibaba-seata 中包含了 io.seata:seata-spring-boot-starter 依賴。若于期望依賴 seata 的版本不一緻,可手動排除重引入。com.alibaba.cloud 原生 版本說明。

parent:

<dependencyManagement>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    </dependencyManagement>
	
	module:
	 <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>
           

添加配置檔案,具體配置項說明參考此處。

seata:
   enabled: true
   application-id: business
   tx-service-group: my_test_tx_group
   config:
      type: nacos
      nacos:
         namespace: "sandbox-seata"
         serverAddr: 139.196.203.133:8848
         group: SEATA_GROUP
         username: xxx
         password: xxx
   registry:
      type: nacos
      nacos:
         application: seata-server
         serverAddr: 139.196.203.133:8848
         group: SEATA_GROUP
         namespace: "sandbox-seata"
         username: xxx
         password: xxx
           

在所有業務庫中建立 undo_log 表,不同的資料庫類型腳本參考此處 ,示例中已自動完成建立。

在需要納入分布式事務鍊路的入口service 方法(保證可使 Spring 切面生效的位置亦可)上添加 @GlobalTransactional 注解。