再前不久,我寫了一篇關於分佈式事務中間件Fescar的解析,沒過幾天Fescar團隊對其進行了品牌升級,取名爲Seata(Simpe Extensible Autonomous Transcaction Architecture),而之前的Fescar的英文全稱爲Fast & EaSy Commit And Rollback。能夠看見Fescar從名字上來看更加侷限於Commit和Rollback,而新的品牌名字Seata旨在打造一套一站式分佈式事務解決方案。更換名字以後,我對其將來的發展更有信心。java
這裏先大概回憶一下Seata的整個過程模型:mysql
在以前的文章中對整個角色有個大致的介紹,在這篇文章中我將重點介紹其中的核心角色TC,也就是事務協調器。git
爲何以前一直強調TC是核心呢?那由於TC這個角色就好像上帝同樣,管控着云云衆生的RM和TM。若是TC一旦很差使,那麼RM和TM一旦出現小問題,那一定會亂的一塌糊塗。因此要想了解Seata,那麼必需要了解他的TC。github
那麼一個優秀的事務協調者應該具有哪些能力呢?我以爲應該有如下幾個:redis
下面我也將逐步闡述Seata是如何作到上面四點。sql
Seata-Server總體的模塊圖如上所示:數據庫
首先來說講比較基礎的Discover模塊,又稱服務註冊/發現模塊。咱們將Seata-Sever啓動以後,須要將本身的地址暴露給其餘使用者,那麼就須要咱們這個模塊幫忙。緩存
這個模塊有個核心接口RegistryService,如上圖所示:安全
若是須要添加本身定義的服務註冊/發現,那麼實現這個接口便可。截止目前在社區的不斷開發推進下,已經有四種服務註冊/發現,分別是redis,zk,nacos,eruka。下面簡單介紹下Nacos的實現:網絡
step1:校驗地址是否合法
step2:獲取Nacos的Name實例,而後將地址註冊到當前Cluster名稱上面。
unregister接口相似,這裏不作詳解。
step1:獲取當前clusterName名字
step2:判斷當前cluster是否已經獲取過了,若是獲取過就從map中取。
step3:從Nacos拿到地址數據,將其轉換成咱們所須要的。
step4:將咱們事件變更的Listener註冊到Nacos
這個接口比較簡單,具體分兩步:
step1:將clstuer和listener添加進map中。
step2:向Nacos註冊。
配置模塊也是一個比較基礎,比較簡單的模塊。咱們須要配置一些經常使用的參數好比:Netty的select線程數量,work線程數量,session容許最大爲多少等等,固然這些參數再Seata中都有本身的默認設置。
一樣的在Seata中也提供了一個接口Configuration,用來自定義咱們須要的獲取配置的地方:
目前爲止有四種方式獲取Config:File(文件獲取),Nacos,Apollo,ZK。再Seata中首先須要配置registry.conf,來配置conf的類型。實現conf比較簡單這裏就不深刻分析。
存儲層的實現對於Seata是否高性能,是否可靠很是關鍵。 若是存儲層沒有實現好,那麼若是發生宕機,在TC中正在進行分佈式事務處理的數據將會被丟失,既然使用了分佈式事務,那麼其確定不能容忍丟失。若是存儲層實現好了,可是其性能有很大問題,RM可能會發生頻繁回滾那麼其徹底沒法應對高併發的場景。
在Seata中默認提供了文件方式的存儲,下面咱們定義咱們存儲的數據爲Session,而咱們的TM創造的全局事務數據叫GloabSession,RM創造的分支事務叫BranchSession,一個GloabSession能夠擁有多個BranchSession。咱們的目的就是要將這麼多Session存儲下來。
在FileTransactionStoreManager#writeSession代碼中:
上面的代碼主要分爲下面幾步:
咱們將數據提交到隊列以後,咱們接下來須要對其進行消費,代碼以下:
這裏將一個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重建等,這些比較簡單,讀者能夠自行閱讀。
你們知道數據庫實現隔離級別主要是經過鎖來實現的,一樣的再分佈式事務框架Seata中要實現隔離級別也須要經過鎖。通常在數據庫中數據庫的隔離級別一共有四種:讀未提交,讀已提交,可重複讀,串行化。在Seata中能夠保證寫的隔離級別是已提交,而讀的隔離級別通常是未提交,可是提供了達到讀已提交隔離的手段。
Lock模塊也就是Seata實現隔離級別的核心模塊。在Lock模塊中提供了一個接口用於管理咱們的鎖:
其中有三個方法:
層數 | 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,那麼比較是不是本身,若是不是則加鎖失敗。
保證Seata高性能的關鍵之一也是使用了Netty做爲RPC框架,採用默認配置的線程模型以下圖所示:
若是採用默認的基本配置那麼會有一個Acceptor線程用於處理客戶端的連接,會有cpu*2數量的NIO-Thread,再這個線程中不會作業務過重的事情,只會作一些速度比較快的事情,好比編解碼,心跳事件,和TM註冊。一些比較費時間的業務操做將會交給業務線程池,默認狀況下業務線程池配置爲最小線程爲100,最大爲500。
這裏須要提一下的是Seata的心跳機制,這裏是使用Netty的IdleStateHandler完成的,以下:
在Sever端對於寫沒有設置最大空閒時間,對於讀設置了最大空閒時間,默認爲15s,若是超過15s則會將連接斷開,關閉資源。
step1:判斷是不是讀空閒的檢測事件。
step2:若是是則斷開連接,關閉資源。
目前官方沒有公佈HA-Cluster,可是經過一些其餘中間件和官方的一些透露,能夠將HA-Cluster用以下方式設計:
具體的流程以下:
step1:客戶端發佈信息的時候根據transcationId保證同一個transcation是在同一個master上,經過多個Master水平擴展,提供併發處理性能。
step2:在server端中一個master有多個slave,master中的數據近實時同步到slave上,保證當master宕機的時候,還能有其餘slave頂上來能夠用。
固然上述一切都是猜想,具體的設計實現還得等0.5版本以後。目前有一個Go版本的Seata-Server也捐贈給了Seata(還在流程中),其經過raft實現副本一致性,其餘細節不是太清楚。
這個模塊也是一個沒有具體公佈實現的模塊,固然有可能會提供插件口,讓其餘第三方metric接入進來,最近Apache skywalking 正在和Seata小組商討如何接入進來。
上面咱們講了不少Server基礎模塊,想必你們對Seata的實現已經有個大概,接下來我會講解事務協調器具體邏輯是如何實現的,讓你們更加了解Seata的實現內幕。
啓動方法在Server類有個main方法,定義了咱們啓動流程:
step1:建立一個RpcServer,再這個裏面包含了咱們網絡的操做,用Netty實現了服務端。
step2:解析端口號和文件地址。
step3:初始化SessionHoler,其中最重要的重要就是重咱們dataDir這個文件夾中恢復咱們的數據,重建咱們的Session。
step4:建立一個CoorDinator,這個也是咱們事務協調器的邏輯核心代碼,而後將其初始化,其內部初始化的邏輯會建立四個定時任務:
step5: 初始化UUIDGenerator這個也是咱們生成各類ID(transcationId,branchId)的基本類。
step6:將本地IP和監聽端口設置到XID中,初始化rpcServer等待客戶端的鏈接。
啓動流程比較簡單,下面我會介紹分佈式事務框架中的常見的一些業務邏輯Seata是如何處理的。
一次分佈式事務的起始點必定是開啓全局事務,首先咱們看看全局事務Seata是如何實現的:
step1: 根據應用ID,事務分組,名字,超時時間建立一個GloabSession,這個再前面也提到過他和branchSession分別是什麼。
step2:對其添加一個RootSessionManager用於監聽一些事件,這裏要說一下目前在Seata裏面有四種類型的Listener(這裏要說明的是全部的sessionManager都實現了SessionLifecycleListener):
step3:開啓Globalsession
這一步會把狀態變爲Begin,記錄開始時間,而且調用RootSessionManager的onBegin監聽方法,將Session保存到map並寫入到咱們的文件。
step4:最後返回XID,這個XID是由ip+port+transactionId組成的,很是重要,當TM申請到以後須要將這個ID傳到RM中,RM經過XID來決定到底應該訪問哪一臺Server。
當咱們全局事務在TM開啓以後,咱們RM的分支事務也須要註冊到咱們的全局事務之上,這裏看看是如何處理的:
step1:經過transactionId獲取並校驗全局事務是不是開啓狀態。
step2:建立一個新的分支事務,也就是咱們的BranchSession。
step3:對分支事務進行加全局鎖,這裏的邏輯就是使用的咱們鎖模塊的邏輯。
step4:添加branchSession,主要是將其添加到globalSession對象中,並寫入到咱們的文件中。
step5:返回branchId,這個ID也很重要,咱們後續須要用它來回滾咱們的事務,或者對咱們分支事務狀態更新。
分支事務註冊以後,還須要彙報分支事務的後續狀態究竟是成功仍是失敗,在Server目前只是簡單的作一下保存記錄,彙報的目的是,就算這個分支事務失敗,若是TM仍是執意要提交全局事務,那麼再遍歷提交分支事務的時候,這個失敗的分支事務就不須要提交。
當咱們分支事務執行完成以後,就輪到咱們的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中,只要沒有成功就不會被刪除會一直重試,若是是同步提交的會放進異步重試隊列進行重試。
若是咱們的TM決定全局回滾,那麼會走到下面的邏輯:
這個邏輯和提交流程基本一致,能夠看做是他的反向,這裏就不展開講了。
最後在總結一下開始咱們提出了分佈式事務的關鍵4點,Seata究竟是怎麼解決的:
最後但願你們能從這篇文章能瞭解Seata-Server的核心設計原理,固然你也能夠想象若是你本身去實現一個分佈式事務的Server應該怎樣去設計?
seata github地址:https://github.com/seata/seata。
最後這篇文章被我收錄於JGrowing-分佈式事務篇,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:https://github.com/javagrowing/JGrowing 麻煩給個小星星喲。
若是你們以爲這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O: