分佈式事務中間件 Fescar - 全局寫排它鎖解讀

前言

通常,數據庫事務的隔離級別會被設置成 讀已提交,已知足業務需求,這樣對應在Fescar中的分支(本地)事務的隔離級別就是 讀已提交,那麼Fescar中對於全局事務的隔離級別又是什麼呢?若是認真閱讀了 分佈式事務中間件Txc/Fescar-RM模塊源碼解讀 的同窗應該能推斷出來:Fescar將全局事務的默認隔離定義成讀未提交。對於讀未提交隔離級別對業務的影響,想必你們都比較清楚,會讀到髒數據,經典的就是銀行轉帳例子,出現數據不一致的問題。而對於Fescar,若是沒有采起任何其它技術手段,那會出現很嚴重的問題,好比:git

如上圖所示,問最終全局事務A對資源R1應該回滾到哪一種狀態?很明顯,若是再根據UndoLog去作回滾,就會發生嚴重問題:覆蓋了全局事務B對資源R1的變動。那Fescar是如何解決這個問題呢?答案就是 Fescar的全局寫排它鎖解決方案,在全局事務A執行過程當中全局事務B會由於獲取不到全局鎖而處於等待狀態。
對於Fescar的隔離級別,引用官方的一段話來做說明:github

全局事務的隔離性是創建在分支事務的本地隔離級別基礎之上的。
在數據庫本地隔離級別  讀已提交 或以上的前提下,Fescar 設計了由事務協調器維護的  全局寫排他鎖,來保證事務間的  寫隔離,將全局事務默認定義在  讀未提交 的隔離級別上。
咱們對隔離級別的共識是:絕大部分應用在  讀已提交 的隔離級別下工做是沒有問題的。而實際上,這當中又有絕大多數的應用場景,實際上工做在  讀未提交 的隔離級別下一樣沒有問題。
在極端場景下,應用若是須要達到全局的  讀已提交,Fescar 也提供了相應的機制來達到目的。默認,Fescar 是工做在  讀未提交 的隔離級別下,保證絕大多數場景的高效性。

下面,本文將深刻到源碼層面對Fescar全局寫排它鎖實現方案進行解讀。Fescar全局寫排它鎖實現方案在TC(Transaction Coordinator)模塊維護,RM(Resource Manager)模塊會在須要鎖獲取全局鎖的地方請求TC模塊以保證事務間的寫隔離,下面就分紅兩個部分介紹:TC-全局寫排它鎖實現方案、RM-全局寫排它鎖使用sql

1、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中維護。
首先,就先來看一下全局寫排它鎖的結構:併發

private static final ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<Integer, Map<String, Long>>>> LOCK_MAP = new ConcurrentHashMap<~>();

總體上,鎖結構採用Map進行設計,前半段採用ConcurrentHashMap,後半段採用HashMap,最終其實就是作一個鎖佔用標記:在某個ResourceId(數據庫源ID)上某個Tabel中的某個主鍵對應的行記錄的全局寫排它鎖被哪一個全局事務佔用。下面,咱們來看一下具體獲取鎖的源碼:分佈式

如上圖註釋,整個acquireLock邏輯仍是很清晰的,對於分支事務須要的鎖資源,要麼是一次性所有成功獲取,要麼所有失敗,不存在部分紅功部分失敗的狀況。經過上面的解釋,可能會有兩個疑問:ide

1. 爲何鎖結構前半部分採用ConcurrentHashMap,後半部分採用HashMap?函數

前半部分採用ConcurrentHashMap好理解:爲了支持更好的併發處理;疑問的是後半部分爲何不直接採用ConcurrentHashMap,而採用HashMap呢?可能緣由是由於後半部分須要去判斷當前全局事務有沒有佔用PK對應的鎖資源,是一個複合操做,即便採用ConcurrentHashMap仍是避免不了要使用Synchronized加鎖進行判斷,還不如直接使用更輕量級的HashMap。

2. 爲何BranchSession要存儲持有的鎖資源

這個比較簡單,在整個鎖的結構中未體現分支事務佔用了哪些鎖記錄,這樣若是全局事務提交時,分支事務怎麼去釋放所佔用的鎖資源呢?因此在BranchSession保存了分支事務佔用的鎖資源。

下圖展現校驗全局鎖資源可否被獲取邏輯:

下圖展現分支事務釋放全局鎖資源邏輯

以上就是TC模塊中全局寫排它鎖的實現原理:在分支事務註冊時,RM會將當前分支事務所須要的鎖資源一併傳遞過來,TC獲取負責全局鎖資源的獲取(要麼一次性所有成功,要麼所有失敗,不存在部分紅功部分失敗);在全局事務提交時,TC模塊自動將全局事務下的全部分支事務持有的鎖資源進行釋放;同時,爲減小全局寫排它鎖獲取失敗機率,TC模塊對外暴露了校驗鎖資源可否被獲取接口,RM模塊能夠在在適當位置加以校驗,以減小分支事務註冊時失敗機率。

2、RM-全局寫排它鎖使用

在RM模塊中,主要使用了TC模塊全局鎖的兩個功能,一個是校驗全局鎖可否被獲取,一個是分支事務註冊去佔用全局鎖,全局鎖釋放跟RM無關,由TC模塊在全局事務提交時自動釋放。分支事務註冊前,都會去作全局鎖狀態校驗邏輯,以保證分支註冊不會發生鎖衝突。
在執行Update、Insert、Delete語句時,都會在sql執行先後生成數據快照以組織成UndoLog,而生成快照的方式基本上都是採用Select...For Update形式,RM嘗試校驗全局鎖可否被獲取的邏輯就在執行該語句的執行器中:SelectForUpdateExecutor,具體以下圖:

基本邏輯以下:

  1. 執行Select ... For update語句,這樣本地事務就佔用了數據庫對應行鎖,其它本地事務因爲沒法搶佔本地數據庫行鎖,進而也不會去搶佔全局鎖。
  2. 循環掌握校驗全局鎖可否被獲取,因爲全局鎖可能會被先於當前的全局事務獲取,所以須要等以前的全局事務釋放全局鎖資源;若是這裏校驗能獲取到全局鎖,那麼因爲步驟1的緣由,在當前本地事務結束前,其它本地事務是不會去獲取全局鎖的,進而保證了在當前本地事務提交前的分支事務註冊不會由於全局鎖衝突而失敗。

注:細心的同窗可能會發現,對於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模塊中對全局寫排它鎖的使用邏輯,因在真正執行獲取全局鎖資源前會去循環校驗全局鎖資源狀態,保證在實際獲取鎖資源時不會由於鎖衝突而失敗,但這樣其實壞處也很明顯:在鎖衝突比較嚴重時,會增長本地事務數據庫鎖佔用時長,進而給業務接口帶來必定的性能損耗。

3、總結

本文詳細介紹了Fescar爲在 讀未提交 隔離級別下作到 寫隔離 而實現的全局寫排它鎖,包括TC模塊內的全局寫排它鎖的實現原理以及RM模塊內如何對全局寫排它鎖的使用邏輯。在瞭解源碼過程當中,筆者也遺留了兩個問題:

1. 全局寫排它鎖數據結構保存在內存中,若是服務器重啓/宕機了怎麼辦,即TC模塊的高可用方案是什麼呢?

2. 一個Fescar管理的全局事務和一個非Fescar管理的本地事務之間發生鎖衝突怎麼辦?具體問題以下圖,問題是:全局事務A如何回滾?

對於問題1有待繼續研究;對於問題2目前已有答案,但Fescar目前暫未實現,具體就是全局事務A回滾時會報錯,全局事務A內的分支事務A1回滾時會校驗afterImage與當前表中對應行數據是否一致,若是一致才容許回滾,不一致則回滾失敗並報警通知對應業務方,由業務方自行處理。

參考

  1. Fescar官方介紹
  2. fescar鎖設計和隔離級別的理解
  3. 姊妹篇:分佈式事務中間件TXC/Fescar—RM模塊源碼解讀



本文做者:中間件小哥

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索