分布式事务中间件 Fescar - 全局写排它锁解读
前言
一般,數據庫事務的隔離級別會被設置成?讀已提交,已滿足業務需求,這樣對應在Fescar中的分支(本地)事務的隔離級別就是?讀已提交,那么Fescar中對于全局事務的隔離級別又是什么呢?如果認真閱讀了?分布式事務中間件Txc/Fescar-RM模塊源碼解讀?的同學應該能推斷出來:Fescar將全局事務的默認隔離定義成讀未提交。對于讀未提交隔離級別對業務的影響,想必大家都比較清楚,會讀到臟數據,經典的就是銀行轉賬例子,出現數據不一致的問題。而對于Fescar,如果沒有采取任何其它技術手段,那會出現很嚴重的問題,比如:
如上圖所示,問最終全局事務A對資源R1應該回滾到哪種狀態?很明顯,如果再根據UndoLog去做回滾,就會發生嚴重問題:覆蓋了全局事務B對資源R1的變更。那Fescar是如何解決這個問題呢?答案就是?Fescar的全局寫排它鎖解決方案,在全局事務A執行過程中全局事務B會因為獲取不到全局鎖而處于等待狀態。
對于Fescar的隔離級別,引用官方的一段話來作說明:
全局事務的隔離性是建立在分支事務的本地隔離級別基礎之上的。
在數據庫本地隔離級別?讀已提交?或以上的前提下,Fescar 設計了由事務協調器維護的?全局寫排他鎖,來保證事務間的?寫隔離,將全局事務默認定義在?讀未提交?的隔離級別上。
我們對隔離級別的共識是:絕大部分應用在?讀已提交?的隔離級別下工作是沒有問題的。而實際上,這當中又有絕大多數的應用場景,實際上工作在?讀未提交?的隔離級別下同樣沒有問題。
在極端場景下,應用如果需要達到全局的?讀已提交,Fescar 也提供了相應的機制來達到目的。默認,Fescar 是工作在?讀未提交?的隔離級別下,保證絕大多數場景的高效性。
下面,本文將深入到源碼層面對Fescar全局寫排它鎖實現方案進行解讀。Fescar全局寫排它鎖實現方案在TC(Transaction Coordinator)模塊維護,RM(Resource Manager)模塊會在需要鎖獲取全局鎖的地方請求TC模塊以保證事務間的寫隔離,下面就分成兩個部分介紹:TC-全局寫排它鎖實現方案、RM-全局寫排它鎖使用
一、TC—全局寫排它鎖實現方案
首先看一下TC模塊與外部交互的入口,下圖是TC模塊的main函數:
上圖中看出RpcServer處理通信協議相關邏輯,而對于TC模塊真實處理器是DefaultCoordiantor,里面包含了所有TC對外暴露的功能,比如doGlobalBegin(全局事務創建)、doGlobalCommit(全局事務提交)、doGlobalRollback(全局事務回滾)、doBranchReport(分支事務狀態上報)、doBranchRegister(分支事務注冊)、doLockCheck(全局寫排它鎖校驗)等,其中doBranchRegister、doLockCheck、doGlobalCommit就是全局寫排它鎖實現方案的入口。
/** * 分支事務注冊,在注冊過程中會獲取分支事務的全局鎖資源 */ @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())); } /** * 校驗全局鎖能否被獲取到 */ @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())); } /** * 全局事務提交,會將全局事務下的所有分支事務的鎖占用記錄釋放 */ @Override protected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext) throws TransactionException {response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId()))); }上述代碼邏輯最后會被代理到DefualtCore去做執行
如上圖,不管是獲取鎖還是校驗鎖狀態邏輯,最終都會被LockManger所接管,而LockManager的邏輯由DefaultLockManagerImpl實現,所有與全局寫排它鎖的設計都在DefaultLockManagerImpl中維護。
首先,就先來看一下全局寫排它鎖的結構:
整體上,鎖結構采用Map進行設計,前半段采用ConcurrentHashMap,后半段采用HashMap,最終其實就是做一個鎖占用標記:在某個ResourceId(數據庫源ID)上某個Tabel中的某個主鍵對應的行記錄的全局寫排它鎖被哪個全局事務占用。下面,我們來看一下具體獲取鎖的源碼:
如上圖注釋,整個acquireLock邏輯還是很清晰的,對于分支事務需要的鎖資源,要么是一次性全部成功獲取,要么全部失敗,不存在部分成功部分失敗的情況。通過上面的解釋,可能會有兩個疑問:
前半部分采用ConcurrentHashMap好理解:為了支持更好的并發處理;疑問的是后半部分為什么不直接采用ConcurrentHashMap,而采用HashMap呢?可能原因是因為后半部分需要去判斷當前全局事務有沒有占用PK對應的鎖資源,是一個復合操作,即使采用ConcurrentHashMap還是避免不了要使用Synchronized加鎖進行判斷,還不如直接使用更輕量級的HashMap。
這個比較簡單,在整個鎖的結構中未體現分支事務占用了哪些鎖記錄,這樣如果全局事務提交時,分支事務怎么去釋放所占用的鎖資源呢?所以在BranchSession保存了分支事務占用的鎖資源。
下圖展示校驗全局鎖資源能否被獲取邏輯:
下圖展示分支事務釋放全局鎖資源邏輯
以上就是TC模塊中全局寫排它鎖的實現原理:在分支事務注冊時,RM會將當前分支事務所需要的鎖資源一并傳遞過來,TC獲取負責全局鎖資源的獲取(要么一次性全部成功,要么全部失敗,不存在部分成功部分失敗);在全局事務提交時,TC模塊自動將全局事務下的所有分支事務持有的鎖資源進行釋放;同時,為減少全局寫排它鎖獲取失敗概率,TC模塊對外暴露了校驗鎖資源能否被獲取接口,RM模塊可以在在適當位置加以校驗,以減少分支事務注冊時失敗概率。
二、RM-全局寫排它鎖使用
在RM模塊中,主要使用了TC模塊全局鎖的兩個功能,一個是校驗全局鎖能否被獲取,一個是分支事務注冊去占用全局鎖,全局鎖釋放跟RM無關,由TC模塊在全局事務提交時自動釋放。分支事務注冊前,都會去做全局鎖狀態校驗邏輯,以保證分支注冊不會發生鎖沖突。
在執行Update、Insert、Delete語句時,都會在sql執行前后生成數據快照以組織成UndoLog,而生成快照的方式基本上都是采用Select...For Update形式,RM嘗試校驗全局鎖能否被獲取的邏輯就在執行該語句的執行器中:SelectForUpdateExecutor,具體如下圖:
?
基本邏輯如下:
注:細心的同學可能會發現,對于Update、Delete語句對應的UpdateExecutor、DeleteExecutor中會因獲取beforeImage而執行Select..For Update語句,進而會去校驗全局鎖資源狀態,而對于Insert語句對應的InsertExecutor卻沒有相關全局鎖校驗邏輯,原因可能是:因為是Insert,那么對應插入行PK是新增的,全局鎖資源必定未被占用,進而在本地事務提交前的分支事務注冊時對應的全局鎖資源肯定是能夠獲取得到的。
接下來我們再來看看分支事務如何提交,對于分支事務中需要占用的全局鎖資源如何生成和保存的。首先,在執行SQL完業務SQL后,會根據beforeImage和afterImage生成UndoLog,與此同時,當前本地事務所需要占用的全局鎖資源標識也會一同生成,保存在ContentoionProxy的ConnectionContext中,如下圖所示。
在ContentoionProxy.commit中,分支事務注冊時會將ConnectionProxy中的context內保存的需要占用的全局鎖標識一同傳遞給TC進行全局鎖的獲取。
以上,就是RM模塊中對全局寫排它鎖的使用邏輯,因在真正執行獲取全局鎖資源前會去循環校驗全局鎖資源狀態,保證在實際獲取鎖資源時不會因為鎖沖突而失敗,但這樣其實壞處也很明顯:在鎖沖突比較嚴重時,會增加本地事務數據庫鎖占用時長,進而給業務接口帶來一定的性能損耗。
三、總結
本文詳細介紹了Fescar為在?讀未提交?隔離級別下做到?寫隔離?而實現的全局寫排它鎖,包括TC模塊內的全局寫排它鎖的實現原理以及RM模塊內如何對全局寫排它鎖的使用邏輯。在了解源碼過程中,筆者也遺留了兩個問題:
對于問題1有待繼續研究;對于問題2目前已有答案,但Fescar目前暫未實現,具體就是全局事務A回滾時會報錯,全局事務A內的分支事務A1回滾時會校驗afterImage與當前表中對應行數據是否一致,如果一致才允許回滾,不一致則回滾失敗并報警通知對應業務方,由業務方自行處理。
?
#阿里云開年Hi購季#幸運抽好禮!
點此抽獎:https://www.aliyun.com/acts/product-section-2019/yq-lottery?utm_content=g_1000042901
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的分布式事务中间件 Fescar - 全局写排它锁解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 双11奇迹背后的大数据平台,不喧哗,自有
- 下一篇: MSSQL - 最佳实践 - 如何打码隐