本文來自 Apache Seata官方文檔,歡迎訪問官網,查看更多深度文章。
本文來自 Apache Seata官方文檔,歡迎訪問官網,查看更多深度文章。
Apache Seata應用側啟動過程剖析——RM & TM如何與TC建立連接
前言
看過官網 README 的第一張圖片的同學都應該清楚,Seata 協調分布式事務的原理便在于通過其協調器側的 TC,來與應用側的 TM、RM 進行各種通信與交互,來保證分布式事務中,多個事務參與者的數據一致性。那么 Seata 的協調器側與應用側之間,是如何建立連接并進行通信的呢?
沒錯,答案就是 Netty,Netty 作為一款高性能的 RPC 通信框架,保證了 TC 與 RM 之間的高效通信,關于 Netty 的詳細介紹,本文不再展開,今天我們探究的重點,在于應用側在啟動過程中,如何通過一系列 Seata 關鍵模塊之間的協作(如 RPC、Config/Registry Center 等),來建立與協調器側之間的通信
從 GlobalTransactionScanner 說起
我們知道 Seata 提供了多個開發期注解,比如用于開啟分布式事務的@GlobalTransactional、用于聲明 TCC 兩階段服務的@TwoPhraseBusinessAction 等,它們都是基于 Spring AOP 機制,對使用了注解的 Bean 方法分配對應的攔截器進行增強,來完成對應的處理邏輯。而 GlobalTransactionScanner 這個 Spring Bean,就承載著為各個注解分配對應的攔截器的職責,從其 Scanner 的命名,我們也不難推斷出,它是為了在 Spring 應用啟動過程中,對與全局事務(GlobalTransactionScanner)相關的 Bean 進行掃描、處理的。
除此之外,應用側 RPC 客戶端(TMClient、RMClient)初始化、與 TC 建立連接的流程,也是在 GlobalTransactionScanner#afterPropertiesSet()中發起的:
/*** package:io.seata.spring.annotation* class:GlobalTransactionScanner*/@Overridepublic void afterPropertiesSet() {if (disableGlobalTransaction) {if (LOGGER.isInfoEnabled()) {LOGGER.info("Global transaction is disabled.");}return;}//在Bean屬性初始化之后,執行TM、RM的初始化initClient();}
RM & TM 的初始化與連接過程
這里,我們以 RMClient.init()為例說明,TMClient 的初始化過程亦同理。
類關系的設計
查看 RMClient#init()的源碼,我們發現,RMClient 先構造了一個 RmNettyRemotingClient,然后執行其初始化init()方法。而 RmNettyRemotingClient 的構造器和初始化方法,都會逐層調用父類的構造器與初始化方法
/*** RMClient的初始化邏輯* package:io.seata.rm* class:RMClient*/public static void init(String applicationId, String transactionServiceGroup) {//① 首先從RmNettyRemotingClient類開始,依次調用父類的構造器RmNettyRemotingClient rmNettyRemotingClient = RmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup);rmNettyRemotingClient.setResourceManager(DefaultResourceManager.get());rmNettyRemotingClient.setTransactionMessageHandler(DefaultRMHandler.get());//② 然后從RmNettyRemotingClient類開始,依次調用父類的init()rmNettyRemotingClient.init();}
上述 RMClient 系列各類之間的關系以及調用構造器和 init()初始化方法的過程如下圖示意:
那么為何要將 RMClient 設計成這樣較為復雜的繼承關系呢?其實是為了將各層的職責、邊界劃分清楚,使得各層可以專注于特定邏輯處理,實現更好的擴展性,這部分的詳細設計思路,可參考 Seata RPC 模塊重構 PR 的操刀者乘輝兄的文章Seata-RPC 重構之路)
初始化的完整流程
各類的構造器與初始化方法中的主要邏輯,大家可以借助下面這個能表意的序列圖來梳理下,此圖大家也可先跳過不看,在下面我們分析過幾個重點類后,再回頭來看這些類是何時登場、如何交互的協作的。
抓住核心——Channel 的創建
首先我們需要知道,應用側與協調器側的通信是借助 Netty 的 Channel(網絡通道)來完成的,因此通信過程的關鍵在于 Channel 的創建,在 Seata 中,通過池化的方式(借助了 common-pool 中的對象池)方式來創建、管理 Channel。
這里我們有必要簡要介紹下對象池的簡單概念及其在 Seata 中的實現:
涉及到的 common-pool 中的主要類:
- GenericKeydObjectPool<K, V>:KV 泛型對象池,提供對所有對象的存取管理,而對象的創建由其內部的工廠類來完成
- KeyedPoolableObjectFactory<K, V>:KV 泛型對象工廠,負責池化對象的創建,被對象池持有
涉及到的 Seata 中對象池實現相關的主要類:
- 首先,被池化管理的對象就是Channel,對應 common-pool 中的泛型 V
- NettyPoolKey:Channel 對應的 Key,對應 common-pool 中的泛型 K,NettyPoolKey 主要包含兩個信息:
- address:創建 Channel 時,對應的 TC Server 地址
- message:創建 Channel 時,向 TC Server 發送的 RPC 消息體
- GenericKeydObjectPool<NettyPoolKey,Channel>:Channel 對象池
- NettyPoolableFactory:創建 Channel 的工廠類
認識了上述對象池相關的主要類之后,我們再來看看 Seata 中涉及 Channel 管理以及與 RPC 相關的幾個主要類:
- NettyClientChannelManager:
- 持有 Channel 對象池
- 與 Channel 對象池交互,對應用側 Channel 進行管理(獲取、釋放、銷毀、緩存等)
- RpcClientBootstrap:RPC 客戶端核心引導類,持有 Netty 框架的 Bootstrap 對象,具備啟停能力;具有根據連接地址來獲取新 Channel 的能力,供 Channel 工廠類調用
- AbstractNettyRemotingClient:
- 初始化并持有 RpcClientBootstrap
- 應用側 Netty 客戶端的頂層抽象,抽象了應用側 RM/TM 取得各自 Channel 對應的 NettyPoolKey 的能力,供 NettyClientChannelManager 調用
- 初始化 NettyPoolableFactory
了解上述概念后,我們可以把 Seata 中創建 Channel 的過程簡化如下:
看到這里,大家可以回過頭再看看上面的RMClient 的初始化序列圖,應該會對圖中各類的職責、關系,以及整個初始化過程的意圖有一個比較清晰的理解了。
建立連接的時機與流程
那么,RMClient 是何時與 Server 建立連接的呢?
在 RMClient 初始化的過程中,大家會發現,很多 init()方法都設定了一些定時任務,而 Seata 應用側與協調器的重連(連接)機制,就是通過定時任務來實現的:
/*** package:io.seata.core.rpcn.netty* class:AbstractNettyRemotingClient*/public void init() {//設置定時器,定時重連TC ServertimerExecutor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {clientChannelManager.reconnect(getTransactionServiceGroup());}}, SCHEDULE_DELAY_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.MILLISECONDS);if (NettyClientConfig.isEnableClientBatchSendRequest()) {mergeSendExecutorService = new ThreadPoolExecutor(MAX_MERGE_SEND_THREAD,MAX_MERGE_SEND_THREAD,KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(),new NamedThreadFactory(getThreadPrefix(), MAX_MERGE_SEND_THREAD));mergeSendExecutorService.submit(new MergedSendRunnable());}super.init();clientBootstrap.start();}
我們通過跟蹤一次 reconnect 的執行,看看上面探究的幾個類之間是如何協作,完成 RMClient 與 TC 的連接的(實際上首次連接可能發生在 registerResource 的過程中,但流程一致)
這個圖中,大家可以重點關注這幾個點:
- NettyClientChannelManager 執行具體 AbstractNettyRemotingClient 中,獲取 NettyPoolKey 的回調函數(getPoolKeyFunction()):應用側的不同 Client(RMClient 與 TMClient),在創建 Channel 時使用的 Key 不同,使兩者在重連 TC Server 時,發送的注冊消息不同,這也是由兩者在 Seata 中扮演的角色不同而決定的:
- TMClient:扮演事務管理器角色,創建 Channel 時,僅向 TC 發送 TM 注冊請求(RegisterTMRequest)即可
- RMClient:扮演資源管理器角色,需要管理應用側所有的事務資源,因此在創建 Channel 時,需要在發送 RM 注冊請求(RegesterRMRequest)前,獲取應用側所有事務資源(Resource)信息,注冊至 TC Server
- 在 Channel 對象工廠 NettyPoolableFactory 的 makeObject(制造 Channel)方法中,使用 NettyPoolKey 中的兩項信息,完成了兩項任務:
- 使用 NettyPoolKey 的 address 創建新的 Channel
- 使用 NettyPoolKey 的 message 以及新的 Channel 向 TC Server 發送注冊請求,這就是 Client 向 TC Server 的連接(首次執行)或重連(非首次,由定時任務驅動執行)請求
以上內容,就是關于 Seata 應用側的初始化及其與 TC Server 協調器側建立連接的全過程分析。
更深層次的細節,建議大家再根據本文梳理的脈絡和提到的幾個重點,細致地閱讀下源碼,相信定會有更深層次的理解和全新的收獲!
后記:考慮到篇幅以及保持一篇源碼分析文章較為合適的信息量,本文前言中所說的配置、注冊等模塊協作配合并沒有在文章中展開和體現。
在下篇源碼剖析中,我會以配置中心和注冊中心為重點,為大家分析,在 RMClient/TM Client 與 TC Server 建立連接之前,Seata 應用側是如何通過服務發現找到 TC Server、如何從配置模塊獲取各種信息的。