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

1.關於Seata

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

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

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

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

2.Transcation Coordinator

爲何以前一直強調TC是核心呢?那由於TC這個角色就好像上帝同樣,管控着云云衆生的RM和TM。若是TC一旦很差使,那麼RM和TM一旦出現小問題,那一定會亂的一塌糊塗。因此要想了解Seata,那麼必需要了解他的TC。github

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

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

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

2.1 Seata-Server的設計

Seata-Server總體的模塊圖如上所示:數據庫

  • Coordinator Core: 在最下面的模塊是事務協調器核心代碼,主要用來處理事務協調的邏輯,如是否commit,rollback等協調活動。
  • Store:存儲模塊,用來將咱們的數據持久化,防止重啓或者宕機數據丟失。
  • Discover: 服務註冊/發現模塊,用於將Server地址暴露給咱們Client。
  • Config: 用來存儲和查找咱們服務端的配置。
  • Lock: 鎖模塊,用於給Seata提供全局鎖的功能。
  • Rpc:用於和其餘端通訊。
  • HA-Cluster:高可用集羣,目前還沒開源。爲Seata提供可靠的高可用功能。

2.2 Discover

首先來說講比較基礎的Discover模塊,又稱服務註冊/發現模塊。咱們將Seata-Sever啓動以後,須要將本身的地址暴露給其餘使用者,那麼就須要咱們這個模塊幫忙。緩存

這個模塊有個核心接口RegistryService,如上圖所示:安全

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

若是須要添加本身定義的服務註冊/發現,那麼實現這個接口便可。截止目前在社區的不斷開發推進下,已經有四種服務註冊/發現,分別是redis,zk,nacos,eruka。下面簡單介紹下Nacos的實現:網絡

2.2.1 register接口:

step1:校驗地址是否合法

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

unregister接口相似,這裏不作詳解。

2.2.2 lookup接口:

step1:獲取當前clusterName名字

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

step3:從Nacos拿到地址數據,將其轉換成咱們所須要的。

step4:將咱們事件變更的Listener註冊到Nacos

2.2.3 subscribe接口

這個接口比較簡單,具體分兩步:

step1:將clstuer和listener添加進map中。

step2:向Nacos註冊。

2.3 Config

配置模塊也是一個比較基礎,比較簡單的模塊。咱們須要配置一些經常使用的參數好比:Netty的select線程數量,work線程數量,session容許最大爲多少等等,固然這些參數再Seata中都有本身的默認設置。

一樣的在Seata中也提供了一個接口Configuration,用來自定義咱們須要的獲取配置的地方:

  • 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代碼中:

上面的代碼主要分爲下面幾步:

  • step1:生成一個TransactionWriteFuture。
  • step2:將這個futureRequest丟進一個LinkedBlockingQueue中。爲何須要將全部數據都丟進隊列中呢?固然這裏其實也能夠用鎖來實現,再另一個阿里開源的RocketMQ中,使用的鎖。不管是隊列仍是鎖他們的目的是爲了保證單線程寫,這又是爲何呢?有人會解釋說,須要保證順序寫,這樣速度就很快,這個理解是錯誤的,咱們的FileChannel實際上是線程安全的,已經能保證順序寫了。保證單線程寫實際上是爲了讓咱們這個寫邏輯都是單線程的,由於可能有些文件寫滿或者記錄寫數據位置等等邏輯,固然這些邏輯均可以主動加鎖去作,可是爲了實現簡單方便,直接再整個寫邏輯加鎖是最爲合適的。
  • step3:調用future.get,等待咱們該條數據寫邏輯完成通知。

咱們將數據提交到隊列以後,咱們接下來須要對其進行消費,代碼以下:

這裏將一個WriteDataFileRunnable()提交進咱們的線程池,這個Runnable的run()方法以下:

分爲下面幾步:

step1: 判斷是否中止,若是stopping爲true則返回null。

step2:從咱們的隊列中獲取數據。

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

step4:將咱們的數據寫進文件,此時數據還在pageCahce層並無刷新到磁盤,若是寫成功而後根據條件判斷是否進行刷盤操做。

step5:當寫入數量到達必定的時候,或者寫入時間到達必定的時候,須要將咱們當前的文件保存爲歷史文件,刪除之前的歷史文件,而後建立新的文件。這一步是爲了防止咱們文件無限增加,大量無效數據浪費磁盤資源。

在咱們的writeDataFile中有以下代碼:

step1:首先獲取咱們的ByteBuffer,若是超出最大循環BufferSize就直接建立一個新的,不然就使用咱們緩存的Buffer。這一步能夠很大的減小GC。

step2:而後將數據添加進入ByteBuffer。

step3:最後將ByteBuffer寫入咱們的fileChannel,這裏會重試三次。此時的數據還在pageCache層,受兩方面的影響,OS有本身的刷新策略,可是這個業務程序不能控制,爲了防止宕機等事件出現形成大量數據丟失,因此就須要業務本身控制flush。下面是flush的代碼:

這裏flush的條件寫入必定數量或者寫的時間超過必定時間,這樣也會有個小問題若是是停電,那麼pageCache中有可能還有數據並無被刷盤,會致使少許的數據丟失。目前還不支持同步模式,也就是每條數據都須要作刷盤操做,這樣能夠保證每條消息都落盤,可是性能也會受到極大的影響,固然後續會不斷的演進支持。

咱們的store核心流程主要是上面幾個方法,固然還有一些好比,session重建等,這些比較簡單,讀者能夠自行閱讀。

2.5 Lock

你們知道數據庫實現隔離級別主要是經過鎖來實現的,一樣的再分佈式事務框架Seata中要實現隔離級別也須要經過鎖。通常在數據庫中數據庫的隔離級別一共有四種:讀未提交,讀已提交,可重複讀,串行化。在Seata中能夠保證寫的隔離級別是已提交,而讀的隔離級別通常是未提交,可是提供了達到讀已提交隔離的手段。

Lock模塊也就是Seata實現隔離級別的核心模塊。在Lock模塊中提供了一個接口用於管理咱們的鎖:

其中有三個方法:

  • acquireLock:用於對咱們的BranchSession加鎖,這裏雖然是傳的分支事務Session,其實是對分支事務的資源加鎖,成功返回true。
  • isLockable:根據事務ID,資源Id,鎖住的Key來查詢是否已經加鎖。
  • cleanAllLocks:清除全部的鎖。 對於鎖咱們能夠在本地實現,也能夠經過redis或者mysql來幫助咱們實現。官方默認提供了本地全局鎖的實現: 在本地鎖的實現中有兩個常量須要關注:
  • 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框架,採用默認配置的線程模型以下圖所示:

若是採用默認的基本配置那麼會有一個Acceptor線程用於處理客戶端的連接,會有cpu*2數量的NIO-Thread,再這個線程中不會作業務過重的事情,只會作一些速度比較快的事情,好比編解碼,心跳事件,和TM註冊。一些比較費時間的業務操做將會交給業務線程池,默認狀況下業務線程池配置爲最小線程爲100,最大爲500。

這裏須要提一下的是Seata的心跳機制,這裏是使用Netty的IdleStateHandler完成的,以下:

在Sever端對於寫沒有設置最大空閒時間,對於讀設置了最大空閒時間,默認爲15s,若是超過15s則會將連接斷開,關閉資源。

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

step2:若是是則斷開連接,關閉資源。

2.7 HA-Cluster

目前官方沒有公佈HA-Cluster,可是經過一些其餘中間件和官方的一些透露,能夠將HA-Cluster用以下方式設計:

具體的流程以下:

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方法,定義了咱們啓動流程:

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是如何實現的:

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

這一步會把狀態變爲Begin,記錄開始時間,而且調用RootSessionManager的onBegin監聽方法,將Session保存到map並寫入到咱們的文件。

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

3.3 BranchRegister-分支事務註冊

當咱們全局事務在TM開啓以後,咱們RM的分支事務也須要註冊到咱們的全局事務之上,這裏看看是如何處理的:

step1:經過transactionId獲取並校驗全局事務是不是開啓狀態。

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

step3:對分支事務進行加全局鎖,這裏的邏輯就是使用的咱們鎖模塊的邏輯。

step4:添加branchSession,主要是將其添加到globalSession對象中,並寫入到咱們的文件中。

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

分支事務註冊以後,還須要彙報分支事務的後續狀態究竟是成功仍是失敗,在Server目前只是簡單的作一下保存記錄,彙報的目的是,就算這個分支事務失敗,若是TM仍是執意要提交全局事務,那麼再遍歷提交分支事務的時候,這個失敗的分支事務就不須要提交。

3.4 GlobalCommit - 全局提交

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

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決定全局回滾,那麼會走到下面的邏輯:

這個邏輯和提交流程基本一致,能夠看做是他的反向,這裏就不展開講了。

4.總結

最後在總結一下開始咱們提出了分佈式事務的關鍵4點,Seata究竟是怎麼解決的:

  • 正確的協調:經過後臺定時任務各類正確的重試,而且將來會推出監控平臺有可能能夠手動回滾。
  • 高可用: 經過HA-Cluster保證高可用。
  • 高性能:文件順序寫,RPC經過netty實現,Seata將來能夠水平擴展,提升處理性能。
  • 高擴展性:提供給用戶能夠自由實現的地方,好比配置,服務發現和註冊,全局鎖等等。

最後但願你們能從這篇文章能瞭解Seata-Server的核心設計原理,固然你也能夠想象若是你本身去實現一個分佈式事務的Server應該怎樣去設計?

seata github地址:https://github.com/seata/seata。

最後這篇文章被我收錄於JGrowing-分佈式事務篇,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:https://github.com/javagrowing/JGrowing 麻煩給個小星星喲。

若是你們以爲這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章
相關標籤/搜索