天天看點

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

緣起

現在用的比較多的分布式事務方案主要有TCC和可靠消息。TCC需要對業務進行改造,分别實作try/confirm/cancel方法,侵入性太強,工作量大。而可靠消息主要适合可以異步調用的業務,對于需要跨服務同步調用的業務,實作困難。以訂機票為例,從深圳->新疆烏魯木齊,假設沒有直達,第一步先要确定中轉地,選擇一家航空公司訂票,假設這步的訂票結果是成功調用南航的服務訂到了深圳->西安,那第二步調用就要根據第一步的結果訂第二張票,比如優先選擇同一家航空公司,間隔時間要大于兩小時,出發地要限制在西安。如果第二步訂票失敗,那麼第一步訂的票也需要復原。像這種存在上下文邏輯關系的跨服務調用,用消息同步方案很難處理。

是以需要一個好的支援跨服務調用的分布式方案。

理想實作

1) 簡潔明了,最好和普通的無事務或一階段事務的遠端調用相接近

2) 侵入性小,盡量不要因為引進分布式事務引起業務代碼結構的明顯變化

3) 危險期短

4) 性能損失少

現實情況

TCC方案滿足條件3,4,條件1勉強,不滿足條件2。

Atomikos方案貌似也比較流行,但有很多限制。比較奇怪的是網上的例子基本上是用Atomikos實作基于XA的單例多資料源的事務控制,看不到跨服務調用的例子。官方倒是提供了各種場景的使用例子,看了後,我猜到了原因:官方的例子很不清爽,而且高性能方案要收費。我在測試的時候發現免費版限制了50個事務/每秒,就直接放棄了對它的進一步研究。

我最滿意的方案是阿裡的GTS,它是基于XA的兩階段送出,使用時隻要一個注解和極少量的代碼侵入,不破壞原有代碼結構,而且性能損失隻有10%左右。看它的文檔,我估計它是直接本地一階段送出,同時記錄操作前像,後像undo日志,當需要復原時,使用undo日志定位記錄,對比目前資料和後像,如果一緻,直接使用前像覆寫復原,如果不一緻,則報警讓人工介入。這個方案很對我胃口,然而不是免費開源,且要在雲端使用。

自己造輪子

TPCTransaction: https://github.com/johnhuang-cn/TPCTransaction

TPC == Two Phase Commit

先看成品效果(基于Spring Cloud)

以經典的銀行轉帳為例,從alpha銀行帳戶轉出到bravo銀行同名帳戶。

調用方代碼

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

服務端Service實作

使用Mybatis mapper

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

調用方是不是和Spring的手工事務控制代碼差不多?服務端隻是将Spring的@Transactional換成了@TPCTransactional注解。

再看看性能

為了對比,同時做了一個一階段送出的實作,也就是說調用即送出,沒有後期同步commit/rollback的功能。如果出現轉出成功,轉入失敗,那就會出現資料不一緻。測試進行了1000次的轉帳業務,資料庫兩個銀行各有50個帳戶,兩邊各賬戶初始存款100萬,為了測試事務的有效性,引入3%的随機失敗。

測試結果(Transaction/Second):

Non transaction serial TPS: 35.57 // 單線程

Non transaction paralle TPS: 337.60 // 并發

TPC transaction serial TPS: 42.34 // 單線程

TPC transaction paralle TPS: 282.16 // 并發

基于TPC的轉帳,經過1000次轉帳并等待10秒後(需待事務完成或逾時復原),兩邊帳戶資料一緻(加起來200萬),證明事務控制是有效的。而一階段直接送出在有随機異常的情況下,毫無疑問肯定是不一緻了。

TPC事務在并發情況下,大約損失15%,這個是可以接受的。線程情況下TPC事務還更快一點。這是因為單線程下兩個方案的鎖的粒度是一樣的,而@TPCTransactional目前實作還比較簡單,@TPCTransactional的代碼比@Transactional少很多很多,是以性能還快一點。但在并發下,鎖的粒度成了影響性能的關鍵,這時兩階段方案就相對慢了。

以上測試,所有的執行個體和DB都在一台機子上,Mysql運作在虛拟機的Docker裡,是以絕對性能不高,主要看相對性能。

實作原理

先上原理圖

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

發起方的實作

事務的開始

TPCTransactionManager.begin(),建立了全局事務UID,并儲存ThreadLocal裡

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

遠端調用的背後

當通過feign client調用遠端服務時,如果目前線程存在未完成的TPC事務,則将事務ID和剩餘timeout通過頭部傳給服務方。通過Header傳送,避免了修改調用參數,減少對代碼的侵入。

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

服務端@TPCTransactional的實作

@TPCTransactional實作了around注入,将被注解的對象方法轉換成異步執行。主線程啟動Executor後,就把自己hold住了。

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

而executor執行完目标方法後,事務并不送出,而隻是将結果傳回給主線程,并把主線程喚醒,接着把自己hold住。主線程拿到結果後,直接将結果傳回給調用方交差,完成了使命。executor還需要繼續等待調用方最終的commit/rollback指令。

發起方的commit/rollback

調用方(也是事務發起方)繼續其它業務邏輯和遠端調用,各參與的遠端服務也一個個将結果傳回後将事務異步挂起。最後一切順利的話,發起方調用TPCTransactionManager.commit()/rollback()。該方法将commit/rollback指令同時發向各參與方。這裡使用了并發發送,以減少危險期。

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

服務方的TrasactionController

TPCTransaction的服務端安排了TransactionController來監聽commit/rollback指令,根據全局transaction id檢查本地是否存在隸屬于該ID的executor,若有将該executor喚醒,executor解除鎖定後完成最後的commit/rollback操作。

基于Spring Cloud的兩階段送出分布式事務架構緣起理想實作現實情況自己造輪子實作原理

其它細節

因為發起方在commit/rollback前有可能當機,那麼服務端的executor會一直鎖定資源,是以必須設定timeout。逾時後,服務端的ExecutorManager自行喚醒executor完成復原以解除記錄鎖定。

在Spring Cloud環境下,每個服務端有可能存在多個執行個體,在Loadbalance作用下,調用和發送commit/rollback可能會被負載到不同的執行個體上。是以TPCTransaction架構對預設的RibbonRule進行了wrapper,對同一線程的調用始終傳回同一個執行個體。和普通的本地事務一樣,TPC發起端的事務也必須在同一個線程完成。

其它細節請移步參觀Github上的項目源碼。

TODO

不論TCC還是其它兩階段方案,都存在危險期,最後送出的時候,若某一服務端當機或網絡故障,會存在部分送出的問題,這種情況需要記錄日志,并報警待人工介入。TPCTransaction架構還未實作事務日志,有待繼續完善。

繼續閱讀