深度剖析一站式分散式事務方案Seata(Fescar)-Server
1.關於Seata
再前不久,我寫了一篇關於分散式事務中介軟體Fescar的解析,沒過幾天Fescar團隊對其進行了品牌升級,取名為Seata(Simpe Extensible Autonomous Transcaction Architecture),而以前的Fescar的英文全稱為Fast & EaSy Commit And Rollback。可以看見Fescar從名字上來看更加侷限於Commit和Rollback,而新的品牌名字Seata旨在打造一套一站式分散式事務解決方案。更換名字之後,我對其未來的發展更有信心。
這裡先大概回憶一下Seata的整個過程模型:
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-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地址:。
最後這篇文章被我收錄於JGrowing-分散式事務篇,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:
麻煩給個小星星喲。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555607/viewspace-2640669/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深度剖析一站式分散式事務方案 Seata(Fescar)-Server分散式Server
- 深度剖析一站式分散式事務方案Seata-Cient分散式
- 深度剖析分散式事務效能分散式
- seata 分散式事務分散式
- 深度剖析Saga分散式事務分散式
- 解密分散式事務框架-Fescar解密分散式框架
- 微服務分散式事務元件 Seata(一)微服務分散式元件
- 來了!阿里開源分散式事務解決方案 Fescar阿里分散式
- 來了!阿里開源分散式事務解決方案Fescar阿里分散式
- 微服務分散式事務解決方案-開源軟體seata微服務分散式
- 分散式事務之Seata的AT模型分散式模型
- 深度剖析分散式事務之 AT 與 XA 對比分散式
- 基於Seata探尋分散式事務的實現方案分散式
- MSSQL server分散式事務解決方案SQLServer分散式
- 分散式事務 Seata TCC 模式深度解析 | SOFAChannel#4 直播整理分散式模式
- 阿里巴巴開源分散式事務解決方案 Fescar阿里分散式
- seata分散式事務AT模式介紹(二)分散式模式
- Seata搭建與分散式事務入門分散式
- 分散式事務 —— SpringCloud Alibaba Seata分散式SpringGCCloud
- 分散式事務(七)之Seata簡介分散式
- 阿里巴巴開源分散式事務解決方案 FESCAR【轉】阿里分散式
- Seata分散式事務TA模式原始碼解讀分散式模式原始碼
- Seata 分散式事務框架 TCC 模式原始碼分析分散式框架模式原始碼
- 分散式事務與Seate框架(2)——Seata實踐分散式框架
- SpringCloud Alibaba(六) - Seata 分散式事務鎖SpringGCCloud分散式
- SpringCloud Alibaba Seata處理分散式事務SpringGCCloud分散式
- 看了 5 種分散式事務方案,我司最終選擇了 Seata,真香!分散式
- 分散式事務中介軟體Seata的設計原理分散式
- 分散式事務 SEATA-1.4.1 AT模式 配合NACOS 應用分散式模式
- 微服務痛點-基於Dubbo + Seata的分散式事務(AT)模式微服務分散式模式
- Seata 無侵入式分散式事務服務的實現基石-JDBC篇分散式JDBC
- 分散式事務解決方案分散式
- 分散式事務處理方案,微服事務處理方案分散式
- SpringCloudAlibaba分散式事務解決方案Seata實戰與原始碼分析-上SpringGCCloud分散式原始碼
- SpringCloudAlibaba分散式事務解決方案Seata實戰與原始碼分析-中SpringGCCloud分散式原始碼
- SpringCloud系列之整合分散式事務Seata應用篇SpringGCCloud分散式
- Spring Boot 整合 Seata 解決分散式事務問題Spring Boot分散式
- 分散式事務框架 seata-golang 通訊模型詳解分散式框架Golang模型