分布式事务中间件 Fescar - 全局写排它锁解读
前言
一般,數(shù)據(jù)庫(kù)事務(wù)的隔離級(jí)別會(huì)被設(shè)置成?讀已提交,已滿足業(yè)務(wù)需求,這樣對(duì)應(yīng)在Fescar中的分支(本地)事務(wù)的隔離級(jí)別就是?讀已提交,那么Fescar中對(duì)于全局事務(wù)的隔離級(jí)別又是什么呢?如果認(rèn)真閱讀了?分布式事務(wù)中間件Txc/Fescar-RM模塊源碼解讀?的同學(xué)應(yīng)該能推斷出來(lái):Fescar將全局事務(wù)的默認(rèn)隔離定義成讀未提交。對(duì)于讀未提交隔離級(jí)別對(duì)業(yè)務(wù)的影響,想必大家都比較清楚,會(huì)讀到臟數(shù)據(jù),經(jīng)典的就是銀行轉(zhuǎn)賬例子,出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。而對(duì)于Fescar,如果沒(méi)有采取任何其它技術(shù)手段,那會(huì)出現(xiàn)很嚴(yán)重的問(wèn)題,比如:
如上圖所示,問(wèn)最終全局事務(wù)A對(duì)資源R1應(yīng)該回滾到哪種狀態(tài)?很明顯,如果再根據(jù)UndoLog去做回滾,就會(huì)發(fā)生嚴(yán)重問(wèn)題:覆蓋了全局事務(wù)B對(duì)資源R1的變更。那Fescar是如何解決這個(gè)問(wèn)題呢?答案就是?Fescar的全局寫排它鎖解決方案,在全局事務(wù)A執(zhí)行過(guò)程中全局事務(wù)B會(huì)因?yàn)楂@取不到全局鎖而處于等待狀態(tài)。
對(duì)于Fescar的隔離級(jí)別,引用官方的一段話來(lái)作說(shuō)明:
在數(shù)據(jù)庫(kù)本地隔離級(jí)別?讀已提交?或以上的前提下,Fescar 設(shè)計(jì)了由事務(wù)協(xié)調(diào)器維護(hù)的?全局寫排他鎖,來(lái)保證事務(wù)間的?寫隔離,將全局事務(wù)默認(rèn)定義在?讀未提交?的隔離級(jí)別上。
我們對(duì)隔離級(jí)別的共識(shí)是:絕大部分應(yīng)用在?讀已提交?的隔離級(jí)別下工作是沒(méi)有問(wèn)題的。而實(shí)際上,這當(dāng)中又有絕大多數(shù)的應(yīng)用場(chǎng)景,實(shí)際上工作在?讀未提交?的隔離級(jí)別下同樣沒(méi)有問(wèn)題。
在極端場(chǎng)景下,應(yīng)用如果需要達(dá)到全局的?讀已提交,Fescar 也提供了相應(yīng)的機(jī)制來(lái)達(dá)到目的。默認(rèn),Fescar 是工作在?讀未提交?的隔離級(jí)別下,保證絕大多數(shù)場(chǎng)景的高效性。
下面,本文將深入到源碼層面對(duì)Fescar全局寫排它鎖實(shí)現(xiàn)方案進(jìn)行解讀。Fescar全局寫排它鎖實(shí)現(xiàn)方案在TC(Transaction Coordinator)模塊維護(hù),RM(Resource Manager)模塊會(huì)在需要鎖獲取全局鎖的地方請(qǐng)求TC模塊以保證事務(wù)間的寫隔離,下面就分成兩個(gè)部分介紹:TC-全局寫排它鎖實(shí)現(xiàn)方案、RM-全局寫排它鎖使用
一、TC—全局寫排它鎖實(shí)現(xiàn)方案
首先看一下TC模塊與外部交互的入口,下圖是TC模塊的main函數(shù):
上圖中看出RpcServer處理通信協(xié)議相關(guān)邏輯,而對(duì)于TC模塊真實(shí)處理器是DefaultCoordiantor,里面包含了所有TC對(duì)外暴露的功能,比如doGlobalBegin(全局事務(wù)創(chuàng)建)、doGlobalCommit(全局事務(wù)提交)、doGlobalRollback(全局事務(wù)回滾)、doBranchReport(分支事務(wù)狀態(tài)上報(bào))、doBranchRegister(分支事務(wù)注冊(cè))、doLockCheck(全局寫排它鎖校驗(yàn))等,其中doBranchRegister、doLockCheck、doGlobalCommit就是全局寫排它鎖實(shí)現(xiàn)方案的入口。
/** * 分支事務(wù)注冊(cè),在注冊(cè)過(guò)程中會(huì)獲取分支事務(wù)的全局鎖資源 */ @Override protected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response,RpcContext rpcContext) throws TransactionException {response.setTransactionId(request.getTransactionId());response.setBranchId(core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(),XID.generateXID(request.getTransactionId()), request.getLockKey())); } /** * 校驗(yàn)全局鎖能否被獲取到 */ @Override protected void doLockCheck(GlobalLockQueryRequest request, GlobalLockQueryResponse response, RpcContext rpcContext)throws TransactionException {response.setLockable(core.lockQuery(request.getBranchType(), request.getResourceId(),XID.generateXID(request.getTransactionId()), request.getLockKey())); } /** * 全局事務(wù)提交,會(huì)將全局事務(wù)下的所有分支事務(wù)的鎖占用記錄釋放 */ @Override protected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext) throws TransactionException {response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId()))); }上述代碼邏輯最后會(huì)被代理到DefualtCore去做執(zhí)行
如上圖,不管是獲取鎖還是校驗(yàn)鎖狀態(tài)邏輯,最終都會(huì)被LockManger所接管,而LockManager的邏輯由DefaultLockManagerImpl實(shí)現(xiàn),所有與全局寫排它鎖的設(shè)計(jì)都在DefaultLockManagerImpl中維護(hù)。
首先,就先來(lái)看一下全局寫排它鎖的結(jié)構(gòu):
整體上,鎖結(jié)構(gòu)采用Map進(jìn)行設(shè)計(jì),前半段采用ConcurrentHashMap,后半段采用HashMap,最終其實(shí)就是做一個(gè)鎖占用標(biāo)記:在某個(gè)ResourceId(數(shù)據(jù)庫(kù)源ID)上某個(gè)Tabel中的某個(gè)主鍵對(duì)應(yīng)的行記錄的全局寫排它鎖被哪個(gè)全局事務(wù)占用。下面,我們來(lái)看一下具體獲取鎖的源碼:
如上圖注釋,整個(gè)acquireLock邏輯還是很清晰的,對(duì)于分支事務(wù)需要的鎖資源,要么是一次性全部成功獲取,要么全部失敗,不存在部分成功部分失敗的情況。通過(guò)上面的解釋,可能會(huì)有兩個(gè)疑問(wèn):
1. 為什么鎖結(jié)構(gòu)前半部分采用ConcurrentHashMap,后半部分采用HashMap?
前半部分采用ConcurrentHashMap好理解:為了支持更好的并發(fā)處理;疑問(wèn)的是后半部分為什么不直接采用ConcurrentHashMap,而采用HashMap呢?可能原因是因?yàn)楹蟀氩糠中枰ヅ袛喈?dāng)前全局事務(wù)有沒(méi)有占用PK對(duì)應(yīng)的鎖資源,是一個(gè)復(fù)合操作,即使采用ConcurrentHashMap還是避免不了要使用Synchronized加鎖進(jìn)行判斷,還不如直接使用更輕量級(jí)的HashMap。2. 為什么BranchSession要存儲(chǔ)持有的鎖資源
這個(gè)比較簡(jiǎn)單,在整個(gè)鎖的結(jié)構(gòu)中未體現(xiàn)分支事務(wù)占用了哪些鎖記錄,這樣如果全局事務(wù)提交時(shí),分支事務(wù)怎么去釋放所占用的鎖資源呢?所以在BranchSession保存了分支事務(wù)占用的鎖資源。下圖展示校驗(yàn)全局鎖資源能否被獲取邏輯:
下圖展示分支事務(wù)釋放全局鎖資源邏輯
以上就是TC模塊中全局寫排它鎖的實(shí)現(xiàn)原理:在分支事務(wù)注冊(cè)時(shí),RM會(huì)將當(dāng)前分支事務(wù)所需要的鎖資源一并傳遞過(guò)來(lái),TC獲取負(fù)責(zé)全局鎖資源的獲取(要么一次性全部成功,要么全部失敗,不存在部分成功部分失敗);在全局事務(wù)提交時(shí),TC模塊自動(dòng)將全局事務(wù)下的所有分支事務(wù)持有的鎖資源進(jìn)行釋放;同時(shí),為減少全局寫排它鎖獲取失敗概率,TC模塊對(duì)外暴露了校驗(yàn)鎖資源能否被獲取接口,RM模塊可以在在適當(dāng)位置加以校驗(yàn),以減少分支事務(wù)注冊(cè)時(shí)失敗概率。
二、RM-全局寫排它鎖使用
在RM模塊中,主要使用了TC模塊全局鎖的兩個(gè)功能,一個(gè)是校驗(yàn)全局鎖能否被獲取,一個(gè)是分支事務(wù)注冊(cè)去占用全局鎖,全局鎖釋放跟RM無(wú)關(guān),由TC模塊在全局事務(wù)提交時(shí)自動(dòng)釋放。分支事務(wù)注冊(cè)前,都會(huì)去做全局鎖狀態(tài)校驗(yàn)邏輯,以保證分支注冊(cè)不會(huì)發(fā)生鎖沖突。
在執(zhí)行Update、Insert、Delete語(yǔ)句時(shí),都會(huì)在sql執(zhí)行前后生成數(shù)據(jù)快照以組織成UndoLog,而生成快照的方式基本上都是采用Select...For Update形式,RM嘗試校驗(yàn)全局鎖能否被獲取的邏輯就在執(zhí)行該語(yǔ)句的執(zhí)行器中:SelectForUpdateExecutor,具體如下圖:
基本邏輯如下:
注:細(xì)心的同學(xué)可能會(huì)發(fā)現(xiàn),對(duì)于Update、Delete語(yǔ)句對(duì)應(yīng)的UpdateExecutor、DeleteExecutor中會(huì)因獲取beforeImage而執(zhí)行Select..For Update語(yǔ)句,進(jìn)而會(huì)去校驗(yàn)全局鎖資源狀態(tài),而對(duì)于Insert語(yǔ)句對(duì)應(yīng)的InsertExecutor卻沒(méi)有相關(guān)全局鎖校驗(yàn)邏輯,原因可能是:因?yàn)槭荌nsert,那么對(duì)應(yīng)插入行PK是新增的,全局鎖資源必定未被占用,進(jìn)而在本地事務(wù)提交前的分支事務(wù)注冊(cè)時(shí)對(duì)應(yīng)的全局鎖資源肯定是能夠獲取得到的。
接下來(lái)我們?cè)賮?lái)看看分支事務(wù)如何提交,對(duì)于分支事務(wù)中需要占用的全局鎖資源如何生成和保存的。首先,在執(zhí)行SQL完業(yè)務(wù)SQL后,會(huì)根據(jù)beforeImage和afterImage生成UndoLog,與此同時(shí),當(dāng)前本地事務(wù)所需要占用的全局鎖資源標(biāo)識(shí)也會(huì)一同生成,保存在ContentoionProxy的ConnectionContext中,如下圖所示。
在ContentoionProxy.commit中,分支事務(wù)注冊(cè)時(shí)會(huì)將ConnectionProxy中的context內(nèi)保存的需要占用的全局鎖標(biāo)識(shí)一同傳遞給TC進(jìn)行全局鎖的獲取。
以上,就是RM模塊中對(duì)全局寫排它鎖的使用邏輯,因在真正執(zhí)行獲取全局鎖資源前會(huì)去循環(huán)校驗(yàn)全局鎖資源狀態(tài),保證在實(shí)際獲取鎖資源時(shí)不會(huì)因?yàn)殒i沖突而失敗,但這樣其實(shí)壞處也很明顯:在鎖沖突比較嚴(yán)重時(shí),會(huì)增加本地事務(wù)數(shù)據(jù)庫(kù)鎖占用時(shí)長(zhǎng),進(jìn)而給業(yè)務(wù)接口帶來(lái)一定的性能損耗。
三、總結(jié)
本文詳細(xì)介紹了Fescar為在?讀未提交?隔離級(jí)別下做到?寫隔離?而實(shí)現(xiàn)的全局寫排它鎖,包括TC模塊內(nèi)的全局寫排它鎖的實(shí)現(xiàn)原理以及RM模塊內(nèi)如何對(duì)全局寫排它鎖的使用邏輯。在了解源碼過(guò)程中,筆者也遺留了兩個(gè)問(wèn)題:
1. 全局寫排它鎖數(shù)據(jù)結(jié)構(gòu)保存在內(nèi)存中,如果服務(wù)器重啟/宕機(jī)了怎么辦,即TC模塊的高可用方案是什么呢?
2. 一個(gè)Fescar管理的全局事務(wù)和一個(gè)非Fescar管理的本地事務(wù)之間發(fā)生鎖沖突怎么辦?具體問(wèn)題如下圖,問(wèn)題是:全局事務(wù)A如何回滾?
對(duì)于問(wèn)題1有待繼續(xù)研究;對(duì)于問(wèn)題2目前已有答案,但Fescar目前暫未實(shí)現(xiàn),具體就是全局事務(wù)A回滾時(shí)會(huì)報(bào)錯(cuò),全局事務(wù)A內(nèi)的分支事務(wù)A1回滾時(shí)會(huì)校驗(yàn)afterImage與當(dāng)前表中對(duì)應(yīng)行數(shù)據(jù)是否一致,如果一致才允許回滾,不一致則回滾失敗并報(bào)警通知對(duì)應(yīng)業(yè)務(wù)方,由業(yè)務(wù)方自行處理。
參考
本文作者:中間件小哥
閱讀原文
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的分布式事务中间件 Fescar - 全局写排它锁解读的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Oracle免安装绿色版-PLSQL连接
- 下一篇: 自制一个 elasticsearch-s