簡介: RPC 模組是我最初研究 Seata 原始碼開始的地方,因此我對 Seata 的 RPC 模組有過一些深刻研究,在我研究了一番後,發現 RPC 模組中的程式碼需要進行優化,使得程式碼更加優雅,互動邏輯更加清晰易懂,本著 “讓天下沒有難懂的 RPC 通訊程式碼” 的初衷,我開始了 RPC 模組的重構之路。
RPC 模組是我最初研究 Seata 原始碼開始的地方,因此我對 Seata 的 RPC 模組有過一些深刻研究,在我研究了一番後,發現 RPC 模組中的程式碼需要進行優化,使得程式碼更加優雅,互動邏輯更加清晰易懂,本著 “讓天下沒有難懂的 RPC 通訊程式碼” 的初衷,我開始了 RPC 模組的重構之路。
這裡建議想要深入瞭解 Seata 互動細節的,不妨從 RPC 模組的原始碼入手,RPC 模組相當於 Seata 的中樞,Seata 所有的互動邏輯在 RPC 模組中表現得淋漓盡致。
這次 RPC 模組的重構將會使得 Seata 的中樞變得更加健壯和易於解讀。
重構繼承關係
在 Seata 的舊版本中,RPC 模組的整體結構有點混亂,尤其是在各個類的繼承關係上,主要體現在:
- 直接在 Remoting 類繼承 Netty Handler,使得 Remoting 類與 Netty Handler 處理邏輯耦合在一起。
- 客戶端和服務端的 Reomting 類繼承關係不統一。
- RemotingClient 被 RpcClientBootstrap 實現,而 RemotingServer 卻被 RpcServer 實現,沒有一個獨立的 ServerBootstrap,這個看起來關係非常混亂。
- 有些介面沒必要抽取出來,比如 ClientMessageSender、ClientMessageListener、ServerMessageSender 等介面,因這些介面會增加整體結構繼承關係的複雜性。
針對上面發現的問題,在重構過程中我大致做了如下事情:
- 將 Netty Handler 抽象成一個內部類放在 Remoting 類中。
- 將 RemotingClient 為客戶端頂級介面,定義客戶端與服務端互動的基本方法,抽象一層 AbstractNettyRemotingClient,下面分別有 RmNettyRemotingClient、TmNettyRemotingClient;將 RemotingServer 為服務端頂級介面,定義服務端與客戶端互動的基本方法,實現類 NettyRemotingServer。
- 同時將 ClientMessageSender、ClientMessageListener、ServerMessageSender 等介面方法歸入到 RemotingClient、RemotingServer 中,由 Reomting 類實現 RemotingClient、RemotingServer,統一 Remoting 類繼承關係。
- 新建 RemotingBootstrap 介面,客戶端和服務端分別實現 NettyClientBootstrap、NettyServerBootstrap,將引導類邏輯從 Reomting 類抽離出來。
在最新的 RPC 模組中的繼承關係簡單清晰,用如下類關係圖表示:
- AbstractNettyRemoting:Remoting 類的最頂層抽象,包含了客戶端和服務端公用的成員變數與公用方法,擁有通用的請求方法(文章後面會講到),Processor 處理器呼叫邏輯(文章後面會講到)。
- RemotingClient:客戶端最頂級介面,定義客戶端與服務端互動的基本方法。
- RemotingServer:服務端最頂級介面,定義服務端與客戶端互動的基本方法。
- AbstractNettyRemotingClient:客戶端抽象類,繼承 AbstractNettyRemoting 類並實現了 RemotingClient 介面。
- NettyRemotingServer:服務端實現類,繼承 AbstractNettyRemoting 類並實現了 RemotingServer 介面。
- RmNettyRemotingClient:Rm 客戶端實現類,繼承 AbstractNettyRemotingClient 類。
- TmNettyRemotingClient:Tm 客戶端實現類,繼承 AbstractNettyRemotingClient 類。
同時將客戶端和服務端的引導類邏輯抽象出來,如下類關係圖表示:
- RemotingBootstrap:引導類介面,有 start 和 stop 兩個抽象方法。
- NettyClientBootstrap:客戶端引導實現類。
- NettyServerBootstrap:服務端引導實現類。
解耦處理邏輯
解耦處理邏輯即是將 RPC 互動的處理邏輯從 Netty Handler 中抽離出來,並將處理邏輯抽象成一個個 Processor,為什麼要這麼做呢?我大致講下現在存在的一些問題:
- Netty Handler 與 處理邏輯是糅合在一起的,由於客戶端與服務端都共用了一套處理邏輯,因此為了相容更多的互動,在處理邏輯中你可以看到非常多難以理解的判斷邏輯。
- 在 Seata 的互動中有些請求是非同步處理的,也有一些請求是同步處理的,但是在舊的處理程式碼邏輯中對同步非同步處理的表達非常隱晦,而且難以看明白。
- 無法從程式碼邏輯當中清晰地表達出請求訊息型別與對應的處理邏輯關係。
- 在 Seata 後面的更新迭代中,如果不將處理處理邏輯抽離出來,這部分程式碼想要增加新的互動邏輯,將會非常困難。
在將處理邏輯從 Netty Handler 進行抽離之前,我們先梳理一下 Seata 現有的互動邏輯。
- RM 客戶端請求服務端的互動邏輯:
- TM 客戶端請求服務端的互動邏輯:
- 服務端請求 RM 客戶端的互動邏輯:
從以上的互動圖中可以清晰地看到了 Seata 的互動邏輯。
客戶端總共接收服務端的訊息:
1)服務端請求訊息
- BranchCommitRequest、BranchRollbackRequest、UndoLogDeleteRequest
2)服務端響應訊息
- RegisterRMResponse、BranchRegisterResponse、BranchReportResponse、GlobalLockQueryResponse
- RegisterTMResponse、GlobalBeginResponse、GlobalCommitResponse、GlobalRollbackResponse、GlobalStatusResponse、GlobalReportResponse
- HeartbeatMessage(PONG)
服務端總共接收客戶端的訊息:
1)客戶端請求訊息
- RegisterRMRequest、BranchRegisterRequest、BranchReportRequest、GlobalLockQueryRequest
- RegisterTMRequest、GlobalBeginRequest、GlobalCommitRequest、GlobalRollbackRequest、GlobalStatusRequest、GlobalReportRequest
- HeartbeatMessage(PING)
2)客戶端響應訊息
- BranchCommitResponse、BranchRollbackResponse
基於以上的互動邏輯分析,我們可以將處理訊息的邏輯抽象成若干個 Processor,一個 Processor 可以處理一個或者多個訊息型別的訊息,只需在 Seata 啟動時註冊將訊息型別註冊到 ProcessorTable 中即可,形成一個對映關係,這樣就可以根據訊息型別呼叫對應的 Processor 對訊息進行處理,用如下圖表示:
在抽象 Remoting 類中定一個 processMessage 方法,方法邏輯是根據訊息型別從 ProcessorTable 中拿到訊息型別對應的 Processor。
這樣就成功將處理邏輯從 Netty Handler 中徹底抽離出來了,Handler#channelRead 方法只需要呼叫 processMessage 方法即可,且還可以靈活根據訊息型別動態註冊 Processor 到 ProcessorTable 中,處理邏輯的可擴充套件性得到了極大的提升。
以下是 Processor 的呼叫流程:
1)客戶端
- RmBranchCommitProcessor:處理服務端全域性提交請求。
- RmBranchRollbackProcessor:處理服務端全域性回滾請求。
- RmUndoLogProcessor:處理服務端 undo log 刪除請求。
- ClientOnResponseProcessor:客戶端處理服務端響應請求,如:BranchRegisterResponse、GlobalBeginResponse、GlobalCommitResponse 等。
- ClientHeartbeatProcessor:處理服務端心跳響應。
2)服務端
- RegRmProcessor:處理 RM 客戶端註冊請求。
- RegTmProcessor:處理 TM 客戶端註冊請求。
- ServerOnRequestProcessor:處理客戶端相關請求,如:BranchRegisterRequest、GlobalBeginRequest、GlobalLockQueryRequest 等。
- ServerOnResponseProcessor:處理客戶端相關響應,如:BranchCommitResponse、BranchRollbackResponse 等。
- ServerHeartbeatProcessor:處理客戶端心跳響應。
下面我以 TM 發起全域性事務提交請求為例子,讓大家感受下 Processor 在整個互動中所處的位置:
重構請求方法
在 Seata 的舊版本當中,RPC 的請求方法也是欠缺優雅,主要體現在:
- 請求方法過於雜亂無章,沒有層次感。
- sendAsyncRequest 方法耦合的程式碼太多,邏輯過於混亂,客戶端與服務端都共用了一套請求邏輯,方法中決定是否批量傳送是根據引數 address 是否為 null 決定,決定是否同步請求是根據 timeout 是否大於 0 決定,顯得極為不合理,且批量請求只有客戶端有用到,服務端並沒有批量請求,共用一套請求邏輯還會導致服務端非同步請求也會建立 MessageFuture 放入 futures 中。
- 請求方法名稱風格不統一,比如客戶端 sendMsgWithResponse,服務端卻叫 sendSyncRequest;
針對以上舊版本 RPC 請求方法的各種缺點,我作了以下改動:
- 將請求方法統一放入 RemotingClient、RemotingServer 介面當中,並作為頂級介面;
- 分離客戶端與服務端請求邏輯,將批量請求邏輯單獨抽到客戶端相關請求方法中,使得是否批量傳送不再根據引數 address 是否為 null 決定;
- 由於 Seata 自身的邏輯特點,客戶端服務端請求方法的引數無法統一,可通過抽取通用的同步/非同步請求方法,客戶端和服務端根據自身請求邏輯特點實現自身的同步/非同步請求邏輯,最後再呼叫通用的同步/非同步請求方法,使得同步/非同步請求都有明確的方法,不再根據 timeout 是否大於 0 決定;
- 統一請求名稱風格。
最終,Seata RPC 的請求方法終於看起來更加優雅且有層次感了。
同步請求:
非同步請求:
其它
- 類目錄調整:RPC 模組目錄中還有一個 netty 目錄,也可以從目錄結構中發現 Seata 的初衷是相容多個 RPC 框架,目前只實現了 netty,但發現 netty 模組中有些類並不 ”netty“,且 RPC 跟目錄的類也並不通用,因此需要將相關類的位置進行調整。
- 某些類重新命名,比如 netty 相關類包含 「netty」。
最終 RPC 模組看起來是這樣的:
作者:張乘輝
本文為阿里雲原創內容,未經允許不得轉載