深度剖析一站式分散式事務方案Seata(Fescar)-Server

咖啡拿鐵發表於2019-04-08

1.關於Seata

再前不久,我寫了一篇關於分散式事務中介軟體Fescar的解析,沒過幾天Fescar團隊對其進行了品牌升級,取名為Seata(Simpe Extensible Autonomous Transcaction Architecture),而以前的Fescar的英文全稱為Fast & EaSy Commit And Rollback。可以看見Fescar從名字上來看更加侷限於Commit和Rollback,而新的品牌名字Seata旨在打造一套一站式分散式事務解決方案。更換名字之後,我對其未來的發展更有信心。

這裡先大概回憶一下Seata的整個過程模型:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

  • TM:事務的發起者。用來告訴TC,全域性事務的開始,提交,回滾。
  • RM:具體的事務資源,每一個RM都會作為一個分支事務註冊在TC。
  • TC:事務的協調者。也可以看做是Fescar-servr,用於接收我們的事務的註冊,提交和回滾。

在之前的文章中對整個角色有個大體的介紹,在這篇文章中我將重點介紹其中的核心角色TC,也就是事務協調器。

2.Transcation Coordinator

為什麼之前一直強調TC是核心呢?那因為TC這個角色就好像上帝一樣,管控著云云眾生的RM和TM。如果TC一旦不好使,那麼RM和TM一旦出現小問題,那必定會亂的一塌糊塗。所以要想了解Seata,那麼必須要了解他的TC。

那麼一個優秀的事務協調者應該具備哪些能力呢?我覺得應該有以下幾個:

  • 正確的協調:能正確的協調RM和TM接下來應該做什麼,做錯了應該怎麼辦,做對了應該怎麼辦。
  • 高可用: 事務協調器在分散式事務中很重要,如果不能保證高可用,那麼他也沒有存在的必要了。
  • 高效能:事務協調器的效能一定要高,如果事務協調器效能有瓶頸那麼他所管理的RM和TM那麼會經常遇到超時,從而引起回滾頻繁。
  • 高擴充套件性:這個特點是屬於程式碼層面的,如果是一個優秀的框架,那麼需要給使用方很多自定義擴充套件,比如服務註冊/發現,讀取配置等等。

下面我也將逐步闡述Seata是如何做到上面四點。

2.1 Seata-Server的設計

深度剖析一站式分散式事務方案Seata(Fescar)-Server

Seata-Server整體的模組圖如上所示:

  • Coordinator Core: 在最下面的模組是事務協調器核心程式碼,主要用來處理事務協調的邏輯,如是否commit,rollback等協調活動。
  • Store:儲存模組,用來將我們的資料持久化,防止重啟或者當機資料丟失。
  • Discover: 服務註冊/發現模組,用於將Server地址暴露給我們Client。
  • Config: 用來儲存和查詢我們服務端的配置。
  • Lock: 鎖模組,用於給Seata提供全域性鎖的功能。
  • Rpc:用於和其他端通訊。
  • HA-Cluster:高可用叢集,目前還沒開源。為Seata提供可靠的高可用功能。

2.2 Discover

首先來講講比較基礎的Discover模組,又稱服務註冊/發現模組。我們將Seata-Sever啟動之後,需要將自己的地址暴露給其他使用者,那麼就需要我們這個模組幫忙。

深度剖析一站式分散式事務方案Seata(Fescar)-Server
這個模組有個核心介面RegistryService,如上圖所示:

  • register:服務端使用,進行服務註冊。
  • unregister:服務端使用,一般在JVM關閉鉤子,ShutdownHook中呼叫。
  • subscribe:客戶端使用,註冊監聽事件,用來監聽地址的變化。
  • unsubscribe:客戶端使用,取消註冊監聽事件。
  • looup:客戶端使用,根據key查詢服務地址列表。
  • close:都可以使用,用於關閉Register資源。

如果需要新增自己定義的服務註冊/發現,那麼實現這個介面即可。截止目前在社群的不斷開發推動下,已經有四種服務註冊/發現,分別是redis,zk,nacos,eruka。下面簡單介紹下Nacos的實現:

2.2.1 register介面:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:校驗地址是否合法

step2:獲取Nacos的Name例項,然後將地址註冊到當前Cluster名稱上面。

unregister介面類似,這裡不做詳解。

2.2.2 lookup介面:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:獲取當前clusterName名字

step2:判斷當前cluster是否已經獲取過了,如果獲取過就從map中取。

step3:從Nacos拿到地址資料,將其轉換成我們所需要的。

step4:將我們事件變動的Listener註冊到Nacos

2.2.3 subscribe介面

深度剖析一站式分散式事務方案Seata(Fescar)-Server
這個介面比較簡單,具體分兩步:

step1:將clstuer和listener新增進map中。

step2:向Nacos註冊。

2.3 Config

配置模組也是一個比較基礎,比較簡單的模組。我們需要配置一些常用的引數比如:Netty的select執行緒數量,work執行緒數量,session允許最大為多少等等,當然這些引數再Seata中都有自己的預設設定。

同樣的在Seata中也提供了一個介面Configuration,用來自定義我們需要的獲取配置的地方:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

  • getInt/Long/Boolean/Config():通過dataId來獲取對應的值。
  • putConfig:用於新增配置。
  • removeConfig:刪除一個配置。
  • add/remove/get ConfigListener:新增/刪除/獲取 配置監聽器,一般用來監聽配置的變更。

目前為止有四種方式獲取Config:File(檔案獲取),Nacos,Apollo,ZK。再Seata中首先需要配置registry.conf,來配置conf的型別。實現conf比較簡單這裡就不深入分析。

2.4 Store

儲存層的實現對於Seata是否高效能,是否可靠非常關鍵。 如果儲存層沒有實現好,那麼如果發生當機,在TC中正在進行分散式事務處理的資料將會被丟失,既然使用了分散式事務,那麼其肯定不能容忍丟失。如果儲存層實現好了,但是其效能有很大問題,RM可能會發生頻繁回滾那麼其完全無法應對高併發的場景。

在Seata中預設提供了檔案方式的儲存,下面我們定義我們儲存的資料為Session,而我們的TM創造的全域性事務資料叫GloabSession,RM創造的分支事務叫BranchSession,一個GloabSession可以擁有多個BranchSession。我們的目的就是要將這麼多Session儲存下來。

在FileTransactionStoreManager#writeSession程式碼中:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

上面的程式碼主要分為下面幾步:

  • step1:生成一個TransactionWriteFuture。
  • step2:將這個futureRequest丟進一個LinkedBlockingQueue中。為什麼需要將所有資料都丟進佇列中呢?當然這裡其實也可以用鎖來實現,再另外一個阿里開源的RocketMQ中,使用的鎖。不論是佇列還是鎖他們的目的是為了保證單執行緒寫,這又是為什麼呢?有人會解釋說,需要保證順序寫,這樣速度就很快,這個理解是錯誤的,我們的FileChannel其實是執行緒安全的,已經能保證順序寫了。保證單執行緒寫其實是為了讓我們這個寫邏輯都是單執行緒的,因為可能有些檔案寫滿或者記錄寫資料位置等等邏輯,當然這些邏輯都可以主動加鎖去做,但是為了實現簡單方便,直接再整個寫邏輯加鎖是最為合適的。
  • step3:呼叫future.get,等待我們該條資料寫邏輯完成通知。

我們將資料提交到佇列之後,我們接下來需要對其進行消費,程式碼如下:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

這裡將一個WriteDataFileRunnable()提交進我們的執行緒池,這個Runnable的run()方法如下:

深度剖析一站式分散式事務方案Seata(Fescar)-Server
分為下面幾步:

step1: 判斷是否停止,如果stopping為true則返回null。

step2:從我們的佇列中獲取資料。

step3:判斷future是否已經超時了,如果超時,則設定結果為false,此時我們生產者get()方法會接觸阻塞。

step4:將我們的資料寫進檔案,此時資料還在pageCahce層並沒有重新整理到磁碟,如果寫成功然後根據條件判斷是否進行刷盤操作。

step5:當寫入數量到達一定的時候,或者寫入時間到達一定的時候,需要將我們當前的檔案儲存為歷史檔案,刪除以前的歷史檔案,然後建立新的檔案。這一步是為了防止我們檔案無限增長,大量無效資料浪費磁碟資源。

在我們的writeDataFile中有如下程式碼:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:首先獲取我們的ByteBuffer,如果超出最大迴圈BufferSize就直接建立一個新的,否則就使用我們快取的Buffer。這一步可以很大的減少GC。

step2:然後將資料新增進入ByteBuffer。

step3:最後將ByteBuffer寫入我們的fileChannel,這裡會重試三次。此時的資料還在pageCache層,受兩方面的影響,OS有自己的重新整理策略,但是這個業務程式不能控制,為了防止當機等事件出現造成大量資料丟失,所以就需要業務自己控制flush。下面是flush的程式碼:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

這裡flush的條件寫入一定數量或者寫的時間超過一定時間,這樣也會有個小問題如果是停電,那麼pageCache中有可能還有資料並沒有被刷盤,會導致少量的資料丟失。目前還不支援同步模式,也就是每條資料都需要做刷盤操作,這樣可以保證每條訊息都落盤,但是效能也會受到極大的影響,當然後續會不斷的演進支援。

我們的store核心流程主要是上面幾個方法,當然還有一些比如,session重建等,這些比較簡單,讀者可以自行閱讀。

2.5 Lock

大家知道資料庫實現隔離級別主要是通過鎖來實現的,同樣的再分散式事務框架Seata中要實現隔離級別也需要通過鎖。一般在資料庫中資料庫的隔離級別一共有四種:讀未提交,讀已提交,可重複讀,序列化。在Seata中可以保證寫的隔離級別是已提交,而讀的隔離級別一般是未提交,但是提供了達到讀已提交隔離的手段。

Lock模組也就是Seata實現隔離級別的核心模組。在Lock模組中提供了一個介面用於管理我們的鎖:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

其中有三個方法:

  • acquireLock:用於對我們的BranchSession加鎖,這裡雖然是傳的分支事務Session,實際上是對分支事務的資源加鎖,成功返回true。
  • isLockable:根據事務ID,資源Id,鎖住的Key來查詢是否已經加鎖。
  • cleanAllLocks:清除所有的鎖。 對於鎖我們可以在本地實現,也可以通過redis或者mysql來幫助我們實現。官方預設提供了本地全域性鎖的實現:
    深度剖析一站式分散式事務方案Seata(Fescar)-Server
    在本地鎖的實現中有兩個常量需要關注:
  • BUCKET_PER_TABLE:用來定義每個table有多少個bucket,目的是為了後續對同一個表加鎖的時候減少競爭。
  • LOCK_MAP:這個map從定義上來看非常複雜,裡裡外外套了很多層Map,這裡用個表格具體說明一下:
層數 key value
1-LOCK_MAP resourceId(jdbcUrl) dbLockMap
2- dbLockMap tableName (表名) tableLockMap
3- tableLockMap PK.hashcode%Bucket (主鍵值的hashcode%bucket) bucketLockMap
4- bucketLockMap PK trascationId

可以看見實際上的加鎖在bucketLockMap這個map中,這裡具體的加鎖方法比較簡單就不作詳細闡述,主要是逐步的找到bucketLockMap,然後將當前trascationId塞進去,如果這個主鍵當前有TranscationId,那麼比較是否是自己,如果不是則加鎖失敗。

2.6 Rpc

保證Seata高效能的關鍵之一也是使用了Netty作為RPC框架,採用預設配置的執行緒模型如下圖所示:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

如果採用預設的基本配置那麼會有一個Acceptor執行緒用於處理客戶端的連結,會有cpu*2數量的NIO-Thread,再這個執行緒中不會做業務太重的事情,只會做一些速度比較快的事情,比如編解碼,心跳事件,和TM註冊。一些比較費時間的業務操作將會交給業務執行緒池,預設情況下業務執行緒池配置為最小執行緒為100,最大為500。

這裡需要提一下的是Seata的心跳機制,這裡是使用Netty的IdleStateHandler完成的,如下:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

在Sever端對於寫沒有設定最大空閒時間,對於讀設定了最大空閒時間,預設為15s,如果超過15s則會將連結斷開,關閉資源。

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:判斷是否是讀空閒的檢測事件。

step2:如果是則斷開連結,關閉資源。

2.7 HA-Cluster

目前官方沒有公佈HA-Cluster,但是通過一些其他中介軟體和官方的一些透露,可以將HA-Cluster用如下方式設計:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

具體的流程如下:

step1:客戶端釋出資訊的時候根據transcationId保證同一個transcation是在同一個master上,通過多個Master水平擴充套件,提供併發處理效能。

step2:在server端中一個master有多個slave,master中的資料近實時同步到slave上,保證當master當機的時候,還能有其他slave頂上來可以用。

當然上述一切都是猜測,具體的設計實現還得等0.5版本之後。目前有一個Go版本的Seata-Server也捐贈給了Seata(還在流程中),其通過raft實現副本一致性,其他細節不是太清楚。

2.8 Metrics

這個模組也是一個沒有具體公佈實現的模組,當然有可能會提供外掛口,讓其他第三方metric接入進來,最近Apache skywalking 正在和Seata小組商討如何接入進來。

3.Coordinator Core

上面我們講了很多Server基礎模組,想必大家對Seata的實現已經有個大概,接下來我會講解事務協調器具體邏輯是如何實現的,讓大家更加了解Seata的實現內幕。

3.1 啟動流程

啟動方法在Server類有個main方法,定義了我們啟動流程:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:建立一個RpcServer,再這個裡面包含了我們網路的操作,用Netty實現了服務端。

step2:解析埠號和檔案地址。

step3:初始化SessionHoler,其中最重要的重要就是重我們dataDir這個資料夾中恢復我們的資料,重建我們的Session。

step4:建立一個CoorDinator,這個也是我們事務協調器的邏輯核心程式碼,然後將其初始化,其內部初始化的邏輯會建立四個定時任務:

  • retryRollbacking:重試rollback定時任務,用於將那些失敗的rollback進行重試的,每隔5ms執行一次。
  • retryCommitting:重試commit定時任務,用於將那些失敗的commit進行重試的,每隔5ms執行一次。
  • asyncCommitting:非同步commit定時任務,用於執行非同步的commit,每隔10ms一次。
  • timeoutCheck:超時定時任務檢測,用於檢測超時的任務,然後執行超時的邏輯,每隔2ms執行一次。

step5: 初始化UUIDGenerator這個也是我們生成各種ID(transcationId,branchId)的基本類。

step6:將本地IP和監聽埠設定到XID中,初始化rpcServer等待客戶端的連線。

啟動流程比較簡單,下面我會介紹分散式事務框架中的常見的一些業務邏輯Seata是如何處理的。

3.2 Begin-開啟全域性事務

一次分散式事務的起始點一定是開啟全域性事務,首先我們看看全域性事務Seata是如何實現的:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1: 根據應用ID,事務分組,名字,超時時間建立一個GloabSession,這個再前面也提到過他和branchSession分別是什麼。

step2:對其新增一個RootSessionManager用於監聽一些事件,這裡要說一下目前在Seata裡面有四種型別的Listener(這裡要說明的是所有的sessionManager都實現了SessionLifecycleListener):

  • ROOT_SESSION_MANAGER:最全,最大的,擁有所有的Session。
  • ASYNC_COMMITTING_SESSION_MANAGER:用於管理需要做非同步commit的Session。
  • RETRY_COMMITTING_SESSION_MANAGER:用於管理重試commit的Session。
  • RETRY_ROLLBACKING_SESSION_MANAGER:用於管理重試回滾的Session。 由於這裡是開啟事務,其他SessionManager不需要關注,我們只新增RootSessionManager即可。

step3:開啟Globalsession

深度剖析一站式分散式事務方案Seata(Fescar)-Server

這一步會把狀態變為Begin,記錄開始時間,並且呼叫RootSessionManager的onBegin監聽方法,將Session儲存到map並寫入到我們的檔案。

step4:最後返回XID,這個XID是由ip+port+transactionId組成的,非常重要,當TM申請到之後需要將這個ID傳到RM中,RM通過XID來決定到底應該訪問哪一臺Server。

3.3 BranchRegister-分支事務註冊

當我們全域性事務在TM開啟之後,我們RM的分支事務也需要註冊到我們的全域性事務之上,這裡看看是如何處理的:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:通過transactionId獲取並校驗全域性事務是否是開啟狀態。

step2:建立一個新的分支事務,也就是我們的BranchSession。

step3:對分支事務進行加全域性鎖,這裡的邏輯就是使用的我們鎖模組的邏輯。

step4:新增branchSession,主要是將其新增到globalSession物件中,並寫入到我們的檔案中。

step5:返回branchId,這個ID也很重要,我們後續需要用它來回滾我們的事務,或者對我們分支事務狀態更新。

分支事務註冊之後,還需要彙報分支事務的後續狀態到底是成功還是失敗,在Server目前只是簡單的做一下儲存記錄,彙報的目的是,就算這個分支事務失敗,如果TM還是執意要提交全域性事務,那麼再遍歷提交分支事務的時候,這個失敗的分支事務就不需要提交。

3.4 GlobalCommit - 全域性提交

當我們分支事務執行完成之後,就輪到我們的TM-事務管理器來決定是提交還是回滾,如果是提交,那麼就會走到下面的邏輯:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

step1:首先找到我們的globalSession。如果他為Null證明已經被commit過了,那麼直接冪等操作,返回成功。

step2:關閉我們的GloabSession防止再次有新的branch進來。

step3:如果status是等於Begin,那麼久證明還沒有提交過,改變其狀態為Committing也就是正在提交。

step4:判斷是否是可以非同步提交,目前只有AT模式可以非同步提交,因為是通過Undolog的方式去做的。MT和TCC都需要走同步提交的程式碼。

step5:如果是非同步提交,直接將其放進我們ASYNC_COMMITTING_SESSION_MANAGER,讓其再後臺執行緒非同步去做我們的step6,如果是同步的那麼直接執行我們的step6。

step6:遍歷我們的BranchSession進行提交,如果某個分支事務失敗,根據不同的條件來判斷是否進行重試,非同步不需要重試,因為其本身都在manager中,只要沒有成功就不會被刪除會一直重試,如果是同步提交的會放進非同步重試佇列進行重試。

3.5 GlobalRollback - 全域性回滾

如果我們的TM決定全域性回滾,那麼會走到下面的邏輯:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

這個邏輯和提交流程基本一致,可以看作是他的反向,這裡就不展開講了。

4.總結

最後在總結一下開始我們提出了分散式事務的關鍵4點,Seata到底是怎麼解決的:

  • 正確的協調:通過後臺定時任務各種正確的重試,並且未來會推出監控平臺有可能可以手動回滾。
  • 高可用: 通過HA-Cluster保證高可用。
  • 高效能:檔案順序寫,RPC通過netty實現,Seata未來可以水平擴充套件,提高處理效能。
  • 高擴充套件性:提供給使用者可以自由實現的地方,比如配置,服務發現和註冊,全域性鎖等等。

最後希望大家能從這篇文章能瞭解Seata-Server的核心設計原理,當然你也可以想象如果你自己去實現一個分散式事務的Server應該怎樣去設計?

seata github地址:github.com/seata/seata…

最後這篇文章被我收錄於JGrowing-分散式事務篇,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:github.com/javagrowing… 麻煩給個小星星喲。

如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支援,O(∩_∩)O:

深度剖析一站式分散式事務方案Seata(Fescar)-Server

相關文章