|
本文作者:李釗,公眾號(hào)「咖啡拿鐵」作者,分布式事務(wù) Seata 社區(qū) Contributor。 1.關(guān)于 Seata在前不久,我寫了一篇關(guān)于分布式事務(wù)中間件 Fescar 的解析,沒過幾天 Fescar 團(tuán)隊(duì)對(duì)其進(jìn)行了品牌升級(jí),取名為 Seata(Simpe Extensible Autonomous Transcaction Architecture),而以前的 Fescar 的英文全稱為 Fast & EaSy Commit And Rollback??梢钥匆?Fescar 從名字上來看更加局限于 Commit 和 Rollback,而新的品牌名字 Seata 旨在打造一套一站式分布式事務(wù)解決方案。更換名字之后,我對(duì)其未來的發(fā)展更有信心。 這里先大概回憶一下 Seata 的整個(gè)過程模型:
在之前的文章中對(duì)整個(gè)角色有個(gè)大體的介紹,在這篇文章中我將重點(diǎn)介紹其中的核心角色 TC,也就是事務(wù)協(xié)調(diào)器。 2.Transaction Coordinator為什么之前一直強(qiáng)調(diào) TC 是核心呢?那因?yàn)?TC 這個(gè)角色就好像上帝一樣,管控著蕓蕓眾生的 RM 和 TM。如果 TC 一旦不好使,那么 RM 和 TM 一旦出現(xiàn)小問題,那必定會(huì)亂的一塌糊涂。所以要想了解 Seata,那么必須要了解他的 TC。 那么一個(gè)優(yōu)秀的事務(wù)協(xié)調(diào)者應(yīng)該具備哪些能力呢?我覺得應(yīng)該有以下幾個(gè):
下面我也將逐步闡述 Seata 是如何做到上面四點(diǎn)。 2.1 Seata-Server 的設(shè)計(jì)Seata-Server 整體的模塊圖如上所示:
2.2 Discover首先來講講比較基礎(chǔ)的 Discover 模塊,又稱服務(wù)注冊(cè)/發(fā)現(xiàn)模塊。我們將 Seata-Server 啟動(dòng)之后,需要將自己的地址暴露給其他使用者,那么就需要這個(gè)模塊幫忙。 這個(gè)模塊有個(gè)核心接口 RegistryService,如上圖所示:
如果需要添加自己定義的服務(wù)注冊(cè)/發(fā)現(xiàn),那么實(shí)現(xiàn)這個(gè)接口即可。截止目前在社區(qū)的不斷開發(fā)推動(dòng)下,已經(jīng)有四種服務(wù)注冊(cè)/發(fā)現(xiàn),分別是 redis、zk、nacos、eruka。下面簡(jiǎn)單介紹下 Nacos 的實(shí)現(xiàn): 2.2.1 register 接口step1:校驗(yàn)地址是否合法; step2:獲取 Nacos 的 Name 實(shí)例,然后將地址注冊(cè)到當(dāng)前 Cluster 名稱上面。 unregister 接口類似,這里不做詳解。 2.2.2 lookup 接口step1:獲取當(dāng)前 clusterName 名字; step2:判斷當(dāng)前 Cluster 是否已經(jīng)獲取過了,如果獲取過就從 Map 中??; step3:從 Nacos 拿到地址數(shù)據(jù),將其轉(zhuǎn)換成所需要的; step4:將事件變動(dòng)的 Listener 注冊(cè)到 Nacos。 2.2.3 subscribe 接口這個(gè)接口比較簡(jiǎn)單,具體分兩步: step1:將 Clstuer 和 Listener 添加進(jìn) Map 中; step2:向 Nacos 注冊(cè)。 2.3 Config配置模塊也是一個(gè)比較基礎(chǔ),比較簡(jiǎn)單的模塊。我們需要配置一些常用的參數(shù)比如:Netty 的 Select 線程數(shù)量,Work 線程數(shù)量,Session 允許最大為多少等等,當(dāng)然這些參數(shù)在 Seata 中都有自己的默認(rèn)設(shè)置。 同樣的在 Seata 中也提供了一個(gè)接口 Configuration,用來自定義需要的獲取配置的地方:
目前為止有四種方式獲取 Config:File(文件獲?。?、Nacos、Apollo、ZK。在 Seata 中首先需要配置 registry.conf,來配置 conf 的類型。實(shí)現(xiàn) conf 比較簡(jiǎn)單這里就不深入分析。 2.4 Store存儲(chǔ)層的實(shí)現(xiàn)對(duì)于 Seata 是否高性能,是否可靠非常關(guān)鍵。 在 Seata 中默認(rèn)提供了文件方式的存儲(chǔ),下面定義存儲(chǔ)的數(shù)據(jù)為 Session,而 TM 創(chuàng)造的全局事務(wù)數(shù)據(jù)叫 GlobalSession,RM 創(chuàng)造的分支事務(wù)叫 BranchSession,一個(gè) GlobalSession 可以擁有多個(gè) BranchSession。我們的目的就是要將這么多 Session 存儲(chǔ)下來。 在 FileTransactionStoreManager#writeSession 代碼中: 上面的代碼主要分為下面幾步: step1:生成一個(gè) TransactionWriteFuture。 step2:將這個(gè) futureRequest 丟進(jìn)一個(gè) LinkedBlockingQueue 中。為什么需要將所有數(shù)據(jù)都丟進(jìn)隊(duì)列中呢?當(dāng)然這里其實(shí)也可以用鎖來實(shí)現(xiàn),在另外一個(gè)阿里開源的 RocketMQ 中使用的鎖。不論是隊(duì)列還是鎖,他們的目的是為了保證單線程寫,這又是為什么呢?有人會(huì)解釋說,需要保證順序?qū)懀@樣速度就很快,這個(gè)理解是錯(cuò)誤的,我們的 FileChannel 其實(shí)是線程安全的,已經(jīng)能保證順序?qū)懥恕1WC單線程寫其實(shí)是為了讓這個(gè)寫邏輯都是單線程的,因?yàn)榭赡苡行┪募憹M或者記錄寫數(shù)據(jù)位置等等邏輯,當(dāng)然這些邏輯都可以主動(dòng)加鎖去做,但是為了實(shí)現(xiàn)簡(jiǎn)單方便,直接再整個(gè)寫邏輯加鎖是最為合適的。 step3:調(diào)用 future.get,等待該條數(shù)據(jù)寫邏輯完成通知。 我們將數(shù)據(jù)提交到隊(duì)列之后,接下來需要對(duì)其進(jìn)行消費(fèi),代碼如下: 這里將一個(gè) WriteDataFileRunnable() 提交進(jìn)線程池,這個(gè) Runnable 的 run() 方法如下: 分為下面幾步: step1:判斷是否停止,如果 stopping 為 true 則返回 null。 step2:從隊(duì)列中獲取數(shù)據(jù)。 step3:判斷 future 是否已經(jīng)超時(shí)了,如果超時(shí),則設(shè)置結(jié)果為 false,此時(shí)我們生產(chǎn)者 get() 方法會(huì)接觸阻塞。 step4:將數(shù)據(jù)寫進(jìn)文件,此時(shí)數(shù)據(jù)還在 pageCache 層并沒有刷新到磁盤,如果寫成功然后根據(jù)條件判斷是否進(jìn)行刷盤操作。 step5:當(dāng)寫入數(shù)量到達(dá)一定的時(shí)候,或者寫入時(shí)間到達(dá)一定的時(shí)候,需要將當(dāng)前的文件保存為歷史文件,刪除以前的歷史文件,然后創(chuàng)建新的文件。這一步是為了防止文件無限增長(zhǎng),大量無效數(shù)據(jù)浪費(fèi)磁盤資源。 在 writeDataFile 中有如下代碼:
step1:首先獲取 ByteBuffer,如果超出最大循環(huán) BufferSize 就直接創(chuàng)建一個(gè)新的,否則就使用緩存的 Buffer。這一步可以很大的減少 GC。 step2:然后將數(shù)據(jù)添加進(jìn)入 ByteBuffer。 step3:最后將 ByteBuffer 寫入 fileChannel,這里會(huì)重試三次。此時(shí)的數(shù)據(jù)還在 pageCache 層,受兩方面的影響,OS 有自己的刷新策略,但是這個(gè)業(yè)務(wù)程序不能控制,為了防止宕機(jī)等事件出現(xiàn)造成大量數(shù)據(jù)丟失,所以就需要業(yè)務(wù)自己控制 flush。下面是 flush 的代碼:
這里 flush 的條件寫入一定數(shù)量或者寫的時(shí)間超過一定時(shí)間,這樣也會(huì)有個(gè)小問題如果是停電,那么 pageCache 中有可能還有數(shù)據(jù)并沒有被刷盤,會(huì)導(dǎo)致少量的數(shù)據(jù)丟失。目前還不支持同步模式,也就是每條數(shù)據(jù)都需要做刷盤操作,這樣可以保證每條消息都落盤,但是性能也會(huì)受到極大的影響,當(dāng)然后續(xù)會(huì)不斷的演進(jìn)支持。 Store 核心流程主要是上面幾個(gè)方法,當(dāng)然還有一些比如 Session 重建等,這些比較簡(jiǎn)單,讀者可以自行閱讀。 2.5 Lock大家知道數(shù)據(jù)庫實(shí)現(xiàn)隔離級(jí)別主要是通過鎖來實(shí)現(xiàn)的,同樣的再分布式事務(wù)框架 Seata 中要實(shí)現(xiàn)隔離級(jí)別也需要通過鎖。一般在數(shù)據(jù)庫中數(shù)據(jù)庫的隔離級(jí)別一共有四種:讀未提交、讀已提交、可重復(fù)讀、串行化。在 Seata 中可以保證寫的互斥,而讀的隔離級(jí)別一般是未提交,但是提供了達(dá)到讀已提交隔離的手段。 Lock 模塊也就是 Seata 實(shí)現(xiàn)隔離級(jí)別的核心模塊。在 Lock 模塊中提供了一個(gè)接口用于管理鎖:
其中有三個(gè)方法:
對(duì)于鎖我們可以在本地實(shí)現(xiàn),也可以通過 redis 或者 mysql 來幫助我們實(shí)現(xiàn)。官方默認(rèn)提供了本地全局鎖的實(shí)現(xiàn):
在本地鎖的實(shí)現(xiàn)中有兩個(gè)常量需要關(guān)注:
可以看見實(shí)際上的加鎖在 bucketLockMap 這個(gè) Map 中,這里具體的加鎖方法比較簡(jiǎn)單就不作詳細(xì)闡述,主要是逐步的找到 bucketLockMap ,然后將當(dāng)前 TrascationId 塞進(jìn)去,如果這個(gè)主鍵當(dāng)前有 TranscationId,那么比較是否是自己,如果不是則加鎖失敗。 2.6 RPC保證 Seata 高性能的關(guān)鍵之一也是使用了 Netty 作為 RPC 框架,采用默認(rèn)配置的線程模型如下圖所示:
如果采用默認(rèn)的基本配置那么會(huì)有一個(gè) Acceptor 線程用于處理客戶端的鏈接,會(huì)有 cpu*2 數(shù)量的 NIO-Thread,再這個(gè)線程中不會(huì)做業(yè)務(wù)太重的事情,只會(huì)做一些速度比較快的事情,比如編解碼,心跳事件和TM注冊(cè)。一些比較費(fèi)時(shí)間的業(yè)務(wù)操作將會(huì)交給業(yè)務(wù)線程池,默認(rèn)情況下業(yè)務(wù)線程池配置為最小線程為 100,最大為 500。 這里需要提一下的是 Seata 的心跳機(jī)制,這里是使用 Netty 的 IdleStateHandler 完成的,如下:
在 Server 端對(duì)于寫沒有設(shè)置最大空閑時(shí)間,對(duì)于讀設(shè)置了最大空閑時(shí)間,默認(rèn)為 15s,如果超過 15s 則會(huì)將鏈接斷開,關(guān)閉資源。
step1:判斷是否是讀空閑的檢測(cè)事件; step2:如果是,則斷開鏈接,關(guān)閉資源。 2.7 HA-Cluster目前官方?jīng)]有公布 HA-Cluster,但是通過一些其他中間件和官方的一些透露,可以將 HA-Cluster 用如下方式設(shè)計(jì):
具體的流程如下: step1:客戶端發(fā)布信息的時(shí)候根據(jù) TranscationId 保證同一個(gè) Transcation 是在同一個(gè) Master 上,通過多個(gè) Master 水平擴(kuò)展,提供并發(fā)處理性能。 step2:在 Server 端中一個(gè) Master 有多個(gè) Slave,Master 中的數(shù)據(jù)近實(shí)時(shí)同步到 Slave上,保證當(dāng) Master 宕機(jī)的時(shí)候,還能有其他 Slave 頂上來可以用。 當(dāng)然上述一切都是猜測(cè),具體的設(shè)計(jì)實(shí)現(xiàn)還得等 0.5 版本之后。目前有一個(gè) Go 版本的 Seata-Server 也捐贈(zèng)給了 Seata (還在流程中),其通過 Raft 實(shí)現(xiàn)副本一致性,其他細(xì)節(jié)不是太清楚。 2.8 Metrics & Tracing這個(gè)模塊也是一個(gè)沒有具體公布實(shí)現(xiàn)的模塊,當(dāng)然有可能會(huì)提供插件口,讓其他第三方 metric 接入進(jìn)來。另外最近 Apache SkyWalking 正在和 Seata 小組商討如何接入進(jìn)來。 3.Coordinator Core上面我們講了很多 Server 基礎(chǔ)模塊,想必大家對(duì) Seata 的實(shí)現(xiàn)已經(jīng)有個(gè)大概,接下來我會(huì)講解事務(wù)協(xié)調(diào)器具體邏輯是如何實(shí)現(xiàn)的,讓大家更加了解 Seata 的實(shí)現(xiàn)內(nèi)幕。 3.1 啟動(dòng)流程啟動(dòng)方法在 Server 類有個(gè) main 方法,定義了我們啟動(dòng)流程:
step1:創(chuàng)建一個(gè) RpcServer,再這個(gè)里面包含了我們網(wǎng)絡(luò)的操作,用 Netty 實(shí)現(xiàn)了服務(wù)端。 step2:解析端口號(hào)和文件地址。 step3:初始化 SessionHolder,其中最重要的重要就是重我們 dataDir 這個(gè)文件夾中恢復(fù)我們的數(shù)據(jù),重建我們的Session。 step4:創(chuàng)建一個(gè)CoorDinator,這個(gè)也是我們事務(wù)協(xié)調(diào)器的邏輯核心代碼,然后將其初始化,其內(nèi)部初始化的邏輯會(huì)創(chuàng)建四個(gè)定時(shí)任務(wù):
step5: 初始化 UUIDGenerator 這個(gè)也是我們生成各種 ID(transcationId,branchId) 的基本類。 step6:將本地 IP 和監(jiān)聽端口設(shè)置到 XID 中,初始化 RpcServer 等待客戶端的連接。 啟動(dòng)流程比較簡(jiǎn)單,下面我會(huì)介紹分布式事務(wù)框架中的常見的一些業(yè)務(wù)邏輯 Seata 是如何處理的。 3.2 Begin - 開啟全局事務(wù)一次分布式事務(wù)的起始點(diǎn)一定是開啟全局事務(wù),首先我們看看全局事務(wù) Seata 是如何實(shí)現(xiàn)的:
step1: 根據(jù)應(yīng)用 ID,事務(wù)分組,名字,超時(shí)時(shí)間創(chuàng)建一個(gè) GlobalSession,這個(gè)再前面也提到過他和 branchSession 分別是什么。 step2:對(duì)其添加一個(gè) RootSessionManager 用于監(jiān)聽一些事件,這里要說一下目前在 Seata 里面有四種類型的 Listener (這里要說明的是所有的 sessionManager 都實(shí)現(xiàn)了 SessionLifecycleListener):
step3:開啟 GlobalSession:
這一步會(huì)把狀態(tài)變?yōu)?Begin,記錄開始時(shí)間,并且調(diào)用 RootSessionManager的 onBegin 監(jiān)聽方法,將 Session 保存到 Map 并寫入到我們的文件。 step4:最后返回 XID,這個(gè) XID 是由 ip+port+transactionId 組成的,非常重要,當(dāng) TM 申請(qǐng)到之后需要將這個(gè) ID 傳到 RM 中,RM 通過 XID 來決定到底應(yīng)該訪問哪一臺(tái) Server。 3.3 BranchRegister - 分支事務(wù)注冊(cè)當(dāng)全局事務(wù)在 TM 開啟之后,RM 的分支事務(wù)也需要注冊(cè)到全局事務(wù)之上,這里看看是如何處理的:
step1:通過 transactionId 獲取并校驗(yàn)全局事務(wù)是否是開啟狀態(tài)。 step2:創(chuàng)建一個(gè)新的分支事務(wù),也就是 BranchSession。 step3:對(duì)分支事務(wù)進(jìn)行加全局鎖,這里的邏輯就是使用鎖模塊的邏輯。 step4:添加 branchSession,主要是將其添加到 GlobalSession 對(duì)象中,并寫入到我們的文件中。 step5:返回 branchId,這個(gè) ID 也很重要,我們后續(xù)需要用它來回滾我們的事務(wù),或者對(duì)我們分支事務(wù)狀態(tài)更新。 分支事務(wù)注冊(cè)之后,還需要匯報(bào)分支事務(wù)的后續(xù)狀態(tài)到底是成功還是失敗,在 Server 目前只是簡(jiǎn)單的做一下保存記錄,匯報(bào)的目的是,就算這個(gè)分支事務(wù)失敗,如果 TM 還是執(zhí)意要提交全局事務(wù),那么再遍歷提交分支事務(wù)的時(shí)候,這個(gè)失敗的分支事務(wù)就不需要提交。 3.4 GlobalCommit - 全局提交當(dāng)分支事務(wù)執(zhí)行完成之后,就輪到 TM - 事務(wù)管理器來決定是提交還是回滾,如果是提交,那么就會(huì)走到下面的邏輯:
step1:首先找到 GlobalSession。如果他為 Null 證明已經(jīng)被 Commit 過了,那么直接冪等操作,返回成功。 step2:關(guān)閉 GlobalSession 防止再次有新的 branch 進(jìn)來。 step3:如果 status 是等于 Begin,那么久證明還沒有提交過,改變其狀態(tài)為 Committing 也就是正在提交。 step4:判斷是否是可以異步提交,目前只有AT模式可以異步提交,因?yàn)槭峭ㄟ^ Undolog 的方式去做的。MT 和 TCC 都需要走同步提交的代碼。 step5:如果是異步提交,直接將其放進(jìn) ASYNCCOMMITTINGSESSION_MANAGER,讓其再后臺(tái)線程異步去做 step6,如果是同步的那么直接執(zhí)行 step6。 step6:遍歷 BranchSession 進(jìn)行提交,如果某個(gè)分支事務(wù)失敗,根據(jù)不同的條件來判斷是否進(jìn)行重試,異步不需要重試,因?yàn)槠浔旧矶荚?manager 中,只要沒有成功就不會(huì)被刪除會(huì)一直重試,如果是同步提交的會(huì)放進(jìn)異步重試隊(duì)列進(jìn)行重試。 3.5 GlobalRollback - 全局回滾如果 TM 決定全局回滾,那么會(huì)走到下面的邏輯:
這個(gè)邏輯和提交流程基本一致,可以看作是他的反向,這里就不展開講了。 4.總結(jié)最后再總結(jié)一下開始我們提出了分布式事務(wù)的關(guān)鍵四點(diǎn),Seata 到底是怎么解決的:
最后希望大家能從這篇文章能了解 Seata-Server 的核心設(shè)計(jì)原理,當(dāng)然你也可以想象如果你自己去實(shí)現(xiàn)一個(gè)分布式事務(wù)的 Server 應(yīng)該怎樣去設(shè)計(jì)? 文中涉及的相關(guān)鏈接
|
|
|