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



本文做者李釗,公衆號「咖啡拿鐵」做者,分佈式事務 Seata 社區 Contributor。mysql

1.關於Seata

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

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

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

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

2.Transaction Coordinator

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

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

  • 正確的協調:能正確的協調 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-Server 啓動以後,須要將本身的地址暴露給其餘使用者,那麼就須要這個模塊幫忙。網絡

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

  • register:服務端使用,進行服務註冊。
  • unregister:服務端使用,通常在 JVM 關閉鉤子,ShutdownHook 中調用。
  • subscribe:客戶端使用,註冊監聽事件,用來監聽地址的變化。
  • unsubscribe:客戶端使用,取消註冊監聽事件。
  • lookup:客戶端使用,根據 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:將數據寫進文件,此時數據還在 pageCache 層並無刷新到磁盤,若是寫成功而後根據條件判斷是否進行刷盤操做。

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 & Tracing

這個模塊也是一個沒有具體公佈實現的模塊,固然有可能會提供插件口,讓其餘第三方 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.總結

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

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

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

文中涉及的相關連接

公衆號:金融級分佈式架構(Antfin_SOFA)

相關文章
相關標籤/搜索