Yugabyte事務隔離性實現分析

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

1. 事務

下面是摘自《Principles of Distributed Database Systems, 3rd Edition》中關於事務的一段描述,講述了事務實現所依賴的組件:前端

事務是對數據庫進行一致、可靠訪問的基本單元,做爲一個比較大的原子操做,負責將數據庫從一個狀態轉移到另外一個狀態。爲了知足一致性,須要對數據完整性限制進行定義,而且須要併發控制算法來協調多個事務的執行。併發控制也會處理隔離性的問題,事務的持久性和原子性須要可靠性的支持。持久性是由不一樣的提交協議和提交管理方法實現的;另外爲了知足原子性,還須要開發恰當的恢復協議。ios

事務須要知足ACID屬性,即:原子性、一致性、隔離性、持久性。本篇主要分析隔離性的實現,對其餘三個屬性僅略做說明。算法

  • 原子性:原子性指一個事務內部對數據的全部操做要麼同時生效,要麼同時取消,不能存在部分生效的狀況。原子性是事務最重要的屬性。yugabyte的單行事務的原子性直接交給DocDB來維護,分佈式事務的原子性則經過分佈式事務管理程序來控制(pggate::PgTxnManager),經過相似兩階段提交(2PC)協議的方式來實現。
  • 一致性:一致性主要是針對數據完整性約束層面的問題,好比一個操做完成以後須要知足外鍵完整性約束,或者更具體的,銀行的兩個帳號轉帳須要知足轉帳先後總金額不變的約束等。目前對該部份內容還沒有展開了解。
  • 隔離性:主要是針對多個事務之間併發執行時可能出現的髒讀、幻讀、不可重複讀等問題提出解決。隔離性的解決在必定層面上依賴事務的原子性。本章接下來主要解釋yugabyte的隔離性實現。
  • 持久性:主要是數據持久化層面的問題,事務提交後,數據庫須要保證數據不能丟失、損壞。

2. 隔離級別

2.1 隔離級別的詳細解釋

隔離級別的定義是從數據庫併發訪問所出現問題引發的,下面是來自postgres針對隔離級別的解釋說明:sql

  • dirty read:一個事務會讀到另一個事務還沒有提交的更新
  • nonrepeatable read:一個事務在從新讀取上次讀的數據時,發現數據發生了變化(被其餘commited事務給改了)
  • phantom read:一個事務根據相同條件執行兩次查詢,查出來的數據的條數發生了變化(其餘commited事務添加的)
  • serialization anomaly:一組事務併發執行和按照不一樣順序逐一執行,最終的結果不一樣。

下表列出了每種隔離級別的含義,主要是描述該級別語義下,上述問題是否容許出現:數據庫

img

2.2 yugabyte支持兩種隔離級別

下面解釋了這兩種隔離級別一般的實現手段。後端

  • snapshot isolation緩存

  • serializable

    • S2PL:Strict Two-Phase Locking能夠實現徹底的串行化隔離,可是併發事務處理能力有限。
    • SSI:Serializable Snapshot Isolation,是基於SI改進達到Serializable級別的隔離性。SSI保留了SI的不少優勢,特別是讀不阻塞任何操做,寫不會阻塞讀。事務依然在快照中運行,但增長了對事務間讀寫衝突的監控用於識別事務圖(transaction graph)中的危險結構。當一組併發事務可能產生異常現象(anomaly),系統將經過回滾其中某些事務進行干預以消除anomaly發生的可能。這個過程雖然會致使某些事務的錯誤回滾(不會致使anomaly的事務被誤殺),但能夠確保消除anomaly。 從理論模型看,SSI性能接近SI,遠遠好於S2PL。

2.3 關於 repeatable read和 snapshot isolation

大多數狀況下,都將這兩種隔離級別歸類爲同一種隔離級別,可是兩種隔離級別仍是有細微差異。不過目前主流的數據庫都採用SI,下面是兩種隔離級別分別存在的問題。

repeatable read

這種隔離級別的實現方式是經過鎖來實現的。當T1要按照某個條件查詢數據時,若是查出了10行,就會對這10行數據加鎖。後來的事務若是想改這10行數據,就會申請鎖失敗,所以T1再次讀這10行數據時,內容不會發生變化,這就是repeatable read語義。 

可是若是另一個事務T2插入了一行新的數據,這行數據知足T1的過濾條件,這行數據在T1執行時是沒法加鎖的(由於尚未這行數據),T2提交以後,T1若是再次執行一樣的查詢,會查出11行數據,這就是幻讀問題。

snapshot isolation

SI沒有幻讀問題。SI隔離級別經過快照方式進行數據隔離,所以對於上面的例子,T1執行時的快照不會受T2的影響,T2插入的數據在T1的生命週期中是不會被查詢到的,所以SI隔離級別沒有幻讀問題。

可是SI有Write Skew問題,關於Write Skew問題的例子以下圖,兩個事務:T1要把全部的白球染成黑色,T2要把全部的黑球染成白色。若是是serializable隔離級別,則要麼最終全爲白色,要麼最終全爲黑色,根據事務執行的前後順序而定;若是是SI隔離級別,T1和T2基於同一個快照來執行本身的事務,因爲兩個事務修改的數據不一樣,因此兩個事務不會產生衝突,會分別成功,致使最終的結果與指望不一樣(T1指望全黑,T2指望全白)。也就是說SI隔離級別下,兩個快照會產生遮擋效果。

img

而repeatable read反而沒有Write Skew問題,由於T1提交前,爲了保證repeatable read的語義,是會經過讀鎖的約束,不容許T2修改數據庫中已有球的顏色的。若是在SI上經過鎖來解決Write Skew的問題,會致使純讀事務堵塞寫事務,喪失了SI的讀不堵塞寫的優點。

上述例子轉換成一個可驗證性較強的SQL示例以下:

T1: blacknum = select count(*) from balls where color=black;
T1: if blacknum > 0  update balls set color=black where color=white;
T2: whitenum =   select count(*) from balls where color= white ;
T2: if whitenum  > 0    update balls set color=black where color=black;
T1: commit;
T2: commit;
複製代碼

另一個在yugabyte上測試過的例子:

yugabyte=# select * from users;
 username  | password | age 
-----------+----------+-----
 zhangsan3 | 111111   |  12
 zhangsan1 | 111111   |  11


/* SI 級別測試 */

T1: begin;
T1: select count(*) from users where age = 12;
T1: update users set age=12 where age=11;


T2: begin;

T2: select count(*) from users where age = 11;
T2: update users set age=11 where age =12;
T1: commit;
T2: commit;


yugabyte=# select * from users;
 username | password | age 
-----------+----------+-----
 zhangsan3 | 111111 | 11
 zhangsan1 | 111111 | 12


/* serializable 級別測試 */
T1: begin; set transaction isolation level serializable;
T1: select count(*) from users where age = 12;
T1: update users set age=12 where age=11;


T2: begin; set transaction isolation level serializable;

T2: select count(*) from users where age = 11;
T2: update users set age=11 where age =12;
T1: commit;
T2: commit;  ERROR:  Error during commit: Operation expired: Transaction expired or aborted by a conflict: 40001


yugabyte=# select * from users;
 username  | password | age 
-----------+----------+-----
 zhangsan3 | 111111   |  12
 zhangsan1 | 111111   |  12
複製代碼

3. 併發控制

併發控制是實現隔離級別的手段。好比要實現serializable隔離級別,併發控制算法就須要將全部併發事務嚴格的排序,而後串行調度執行。

3.1 2PL併發控制算法

兩階段提交併發控制已經在理論上證實是能夠實現serializable隔離級別的併發控制算法。可是2PL有2個比較典型的問題:

  • 實現困難
    2PL的鎖管理分兩個階段:申請階段和釋放階段,所以才稱作2PL。2PL要求一個事務的控制流程中,鎖的釋放必須在全部須要的鎖都申請完以後。這對LockManager的實現提出了很高的要求,如何識別出一個事務的鎖已經所有申請完了,從而儘早的釋放鎖。
  • 事務的吞吐能力低
    鎖的方式實現事務併發控制,會致使事務間的衝突增長。只有read-read事務不衝突。

3.2 基於時間排序的併發控制算法(tocc)

這裏之因此排序,主要是要實現serializable隔離級別。這種算法處理衝突的方式是給失敗的事務分配一個新的時間戳,而後重啓該事務,所以TOCC算法不存在死鎖場景,代價是事務會重啓不少次。

基於時間的併發控制算法中,時間戳的生成是最核心的工做。要實現分佈式的全局惟一單調遞增的時間戳是很是困難的工做,目前沒有這方面的解決方案。所以目前的時間戳生成都是每一個節點生成本身的時間戳,這帶來的問題是節點間的時間戳可能不一致,某些節點分配的時間戳落後於其餘節點,這會致使由該節點生成的事務一般會被拒絕而後重啓。

爲了儘可能讓節點間的時間戳保持同步,一個優化方式是節點間經過rpc來互相更新時間戳,兩兩之間選大的那個時間戳。

改進:TO最大的問題是會致使事務過多的重啓,一種改進方式是調度器對要執行的事務進行緩存,而不是當即執行,直到調度器確認不會接受到時間戳更小的請求,此時再對事務進行排序執行。這樣會在必定程度上緩解事務衝突的機率(時間戳小的會被先執行),從而減小事務重啓的機率。不過緩存的方式可能引入死鎖。

3.3 多版本TO併發控制算法(mvtocc)

多版本TO是另一種用來避免重啓事務的算法。基本的思想是:更新操做不修改數據庫,而是建立一個新版本;每一個版本都標記了與該版本相關的事務的時間戳信息(可能包含事務的start和commit時間戳)。

事務管理程序爲每一個事務分配一個時間戳,事務的讀操做會被轉換爲對某個版本的讀(根據事務的當前時間戳來肯定)。對於寫操做,只有一種狀況會被拒絕:已經存在一個時間戳更大(意味着更晚到來,也就是更新)的事務在目標數據上進行讀,這種狀況纔會拒絕這個時間戳較老的事務的執行。

3.4 多版本併發控制(mvcc)

mvcc不對事務進行嚴格排序執行,此種併發控制一般用於實現snapshot isolation級別。SI是目前商業數據庫採用比較多的隔離級別。SI相對serializable,存在一個問題:Write Skew;在大多數場景下SI已經足夠好。

4. 樂觀悲觀

這是針對併發控制算法所採起的機制的一種表述,主要是從性能角度考慮問題。當咱們假設事務之間的衝突時比較頻繁的時候,一般會採起悲觀算法;反之會採起樂觀算法。

悲觀算法不容許兩個事務對同一個數據項進行衝突訪問,所以悲觀算法的執行步驟以下:

有效性驗證 -> 讀 -> 計算 -> 寫

樂觀算法將有效性驗證移到寫以前執行:

讀 -> 計算 -> 有效性驗證 -> 寫

樂觀算法不會堵塞事務的執行,所以具備更高的併發能力。可是若是併發事務之間衝突比較多,可能會致使事務發生較多的重啓,從而影響性能。樂觀算法適合衝突較少發生的併發場景。

一種實現樂觀鎖的方式是在事務有效性驗證的時候分配時間戳,這樣的分配方式產生的結果就是哪一個事務先commit,哪一個事務會得到較早的時間戳;對於樂觀鎖併發控制來講,過早分配時間戳反而會致使沒必要要的衝突斷定發生。

5. 形式化證實

對於併發控制實現的有效性,一般都有比較嚴格的形式化證實。這裏沒有深刻了解。

6. yugabyte的隔離級別實現機制

yugabyte的隔離級別是基於mvcc+鎖類組合實現的(固然從更高的層面來看待這個問題的話,也能夠認爲鎖是實現mvcc的一部分,這裏分紅兩部分來看待有助於更清楚的理解),這裏具體解釋mvcc和鎖在yugabyte的隔離級別實現中分別解決什麼問題:

mvcc
mvcc主要是解決併發訪問問題。傳統的基於鎖的併發訪問控制性能比較差,緣由是傳統的讀鎖會堵塞全部的寫操做,只有讀-讀操做之間不會堵塞。mvcc的提出專門解決這個問題,用戶的讀操做是對特定版本的數據的訪問,寫操做不會修改該版本的數據,所以不會堵塞寫。另外事務的原子性要求在一個事務內提交的全部數據的版本號必須相同,這保證了讀操做不會讀取到事務執行一半後的結果。

若是系統可以保證一個事務在開始階段獲取的數據庫視圖,在整個事務執行過程當中保持不變(除了事務本身產生的變動),那麼系統就完成了多版本(mv)這一層面的工做。可是隻有多版本還不能徹底解決問題,緣由是當多個用戶基於同一個版本的數據進行修改時,仍是會有衝突產生的,解決併發寫衝突問題仍是須要鎖的參與(或者其餘無鎖數據結構)。


當衝突發生時,有不少識別衝突的機制。加鎖是用來規避衝突的一種方式,實現起來比較簡單直觀。好比T1修改row1的column1的值時,會加鎖;此時T2若是想作一樣的操做經過檢查鎖的存在與否就能夠提早檢測到衝突。

另外,yugabyte的不一樣隔離級別的控制也是經過鎖來實現的。yugabyte定義了很是細粒度的鎖類型,不一樣隔離級別根據其語義要求,在數據訪問時加不一樣的鎖。好比:兩個SI級別的寫鎖在語義上是衝突的,即:在SI隔離級別下,兩個事務對同一個對象申請read-for-update的寫鎖是不被支持的,從而不容許此類事務併發執行(或者在申請時不作檢查,最終提交時檢測衝突時讓一個失敗,根據悲觀仍是樂觀控制方法不一樣而不一樣)。而對於serializable隔離級別的寫鎖,語義上是不衝突的,因爲在該隔離級別下事務是串行執行,多個事務同時更新同一個目標數據是被容許併發執行的,最終的結果是latest hybrid timestamp wins。值得指出的是,yugabyte的SI級別的純寫鎖(區別於read-for-update鎖)與serializable隔離基本的寫鎖採用的是同一種鎖: strong serializable write lock 。關於鎖的更具體的定義接下來會詳細講解。

對於衝突事務的處理,若是是在commit階段檢查衝突(樂觀鎖),一般的處理方式是First-Committer-Wins(FCW),後提交的直接失敗,從而規避Lost Update問題;若是是在事務開始階段檢查衝突(悲觀鎖),則一般採用 First-write-wins(FWW)機制,後啓動的事務直接失敗。 yugabyte沒有采用FWW,而是採用優先級的方式,優先級高的事務能夠繼續執行,優先級低的直接失敗。yugabyte的優先級分配是隨機數,兩個事務的優先級高低是隨機的。不過悲觀鎖和樂觀鎖的優先級處於不一樣的區間,悲觀鎖的優先級區間的全部值都高於樂觀鎖。所以一個樂觀鎖事務若是與一個悲觀鎖事務衝突,則樂觀鎖的事務必定會被取消。

總結:以上就是yugabyte隔離級別實現的所有考慮,剩下的就是基於這些考慮的具體實現。

6.1 yugabyte的mvcc實現

數據表示
DocDB的數據在磁盤上是以Tablet爲單位管理的,每一個Tablet對應一個rocksdb文件數據庫實例。 rocksdb是一個kv系統,對於一個特定的Key-Value對,yugabyte經過將事務的時間戳編碼到Key中來標記數據的版本號。

版本管理
yugabyte事務中的數據訪問操做最終都會轉換成對一個或多個Tablet的operation,DocDB接收到的每一個請求都是獨立到某個Tablet的,不存着一個請求操做多個Tablet的狀況,所以一個事務若是須要訪問(修改)多個Tablet的數據,在YQL層面會轉換成多個DocDB的rpc請求,而多個Tablet是可能分佈在不一樣節點上的,這裏就須要YQL經過分佈式事務的方式來管理事務,關於分佈式事務的實現這裏不展開討論,有專門的一篇文章來講明。這裏主要是爲了說明一點:到達DocDB的請求必定是具體到某個Tablet了,因此DocDB的版本管理也是在Tablet內部完成的,不是全局管理。

DocDB經過 operation來抽象全部對Tablet的寫操做,operation能夠看作是DocDB內部的事務。 每一個Tablet內部都有一個MvccManager,MvccManager主要負責時間戳管理,他提供以下特性:

  • 提供接口(AddPending),爲operation分配混合時間戳(LEADER端),並將該時間戳添加到內部的隊列中;
  • 保證後添加的operation的時間戳必定大於前面全部事務的時間戳;
  • 提供接口(SafeTime),獲取一個最大的時間戳,並確保讀操做使用該時間戳讀數據是安全的;(即:該時間戳前的全部數據都已經replicated了)
  • 提供接口(Replicated),要求全部完成replicate的operation都須要調用該接口,將入隊的時間戳出隊。該接口檢查replicated operation的順序,要求operation必須按照添加的順序被 replicated,但執行順序的保證不在這裏,這裏只作過後檢查,至關於一個Assert斷言;
  • 提供接口(Aborted),要求全部aborted的operation要調用該接口,將入隊的時間戳出隊。

6.2 yugabyte的鎖定義和實現

爲了支持SNAPSHOT和SERIALIZABLE兩種隔離級別,yugabyte對鎖進行了以下劃分 :

  • Snapshot isolation write lock: SI隔離級別的事務會在其要修改的value上加該種鎖;
  • Serializable read lock:Serializable隔離級別的事務會在執行read-modify-write類型的操做時,對要read的對象加該種鎖;
  • Serializable write lock: Serializable隔離級別的事務會在其要修改的value上加該種鎖;

能夠看到SI read並不加鎖,所以SI隔離級別的read不會堵塞任何其餘事務。 下面是這三種鎖的衝突矩陣:

img

Fine grained locking:
爲了在更細粒度上減小鎖的衝突,yugabyte按照數據讀寫的特色對鎖進行了更近一步的劃分,好比當修改一行中的某個列的value時,在行上加WeakLock,在要修改的列上加StrongLock;這樣當另外一個事務修改的是同一行的另外一個列的value時,WeakLock之間不衝突, StrongLock也不衝突(由於鎖的是不一樣對象), 因此這個事務能夠併發執行,下面是更細粒度的鎖衝突矩陣:

img

SharedLockManager

SharedLockManager實現了DocDB的fine-grained locking,每一個Tablet維護一個 SharedLockManager實例,負責當前Tablet的鎖控制。

yugabyte的鎖類型定義:

YB_DEFINE_ENUM(IntentType,
    ((kWeakRead,      kWeakIntentFlag |  kReadIntentFlag))
    ((kWeakWrite,     kWeakIntentFlag | kWriteIntentFlag))
    ((kStrongRead,  kStrongIntentFlag |  kReadIntentFlag))
    ((kStrongWrite, kStrongIntentFlag | kWriteIntentFlag))
);
複製代碼

7. yugabyte併發控制工做流程

本節將詳細分析事務的執行流程中涉及併發控制部分的工做機制,在這個流程分析過程當中,咱們將重點關注以下一些問題:

  • mvcc和鎖分別是如何在其中參與工做的
  • 分佈式事務的時間戳是如何實現單調遞增的
  • 分佈式事務、單行事務在不一樣隔離級別下是如何實現併發控制的
  • 基於鎖的衝突檢查機制是否須要考慮多版本的問題
  • Lost Update、Ditry Read、Unrepeatable Read、Phantom Read等問題是如何解決的

7.1 相關類及扮演的角色

Operation
Operation是對寫操做的事務具體操做的抽象,根據操做類型不一樣又派生出具體的Operation對象,如:

  • WriteOperation:全部的寫操做都在這裏封裝。
  • UpdateTxnOperation:全部對status tablet的寫操做都在這裏封裝。

Operation主要封裝了以下接口:

  • Prepare():operation的prepare階段,與2PC的prepare不一樣,這裏不進行任何數據操做,只作一些不涉及狀態修改的準備工做。
  • Start():tablet LEADER 做爲事務的最開始的啓動者,會在Start階段爲事務申請hybrid_time. FOLLOWER則是從LEADER的commit消息中獲取該時間戳,所以Start階段不會從新獲取。
  • Replicated():raft執行完數據複製後,會經過OperationDriver調用該接口,執行數據落盤操做。
  • Aborted():事務失敗會調用該接口。

OperationDriver
OperationDriver是對一個事務執行流程的抽象,全部事務的執行都由OperationDriver來管理,事務的執行流程抽象步驟:

  • Init() :事務開始前會先建立一個OperationDriver對象來管理該事務的執行流程。

  • ExecuteAsync():將OperationDriver提交到Preparer線程並當即返回,後續的執行由Preparer線程控制。

  • PrepareAndStartTask():調用Prepare() and Start()。

  • ReplicationFinished():raft完成複製後調用改回調函數,該回調函數是在Init階段賦值給raft的。

    • ApplyOperation => op->Replicated() => op->DoReplicated():將數據寫到rocksdb中。

HybridTime
用來多版本控制的混合時間戳實現。

MvccManager
MvccManager負責維護每一個tablet的mvcc控制流程。MvccManager維護了一個deque隊列,用來跟蹤全部operation的時間戳, MvccManager要求全部operation的 replicated順序必須與該operation的時間戳的入隊順序保持一致。也就是對同一個tablet上的全部的operation必須按照時間戳申請的前後順序完成。

Tablet
用戶的一個表中的數據會按partition進行分區管理,每一個分區在yugabyte中稱做一個Tablet,raft複製是以tablet爲單位管理的。 每一個tablet維護本身的MvccManager實例,用以管理當前tablet的混合時間戳分配。

SharedLockManager
併發控制的鎖實現。

TransactionCoordinator
負責分佈式事務狀態管理,即:status tablet的讀寫。同時協調TransactionParticipant完成數據最終寫入到normal_db

TransactionParticipant
負責處理APPLYING和CLEANUP請求,這兩個請求一個是commit成功後將數據從intent_db寫入到normal_db,完成最終的數據提交;另外一個是對abort的事務進行rollback操做,清理垃圾數據(從intent_db中刪除)。

7.2 併發控制執行流程

7.2.1 TakeTransaction

該階段是申請事務,會分別在client端和server端建立YBTransaction實例(metadata相同)。

7.2.2 TransactionStatus::CREATED

心跳信息,TakeTransaction以後會當即執行。通知status tablet更新狀態,並維護心跳。

7.2.3 doc-op執行(經過PgDml爲pg封裝接口)

這一步驟是進行數據的讀寫,client端構造doc operation,經過rpc發送給DocDB(TabletService)。事務的隔離級別和鎖的控制都在這裏處理。

申請內存鎖階段(內存鎖只在tablet leader上持有)

這裏針對要修改的目標對象申請鎖的類型進行總結(其父路徑所有申請相應的weak鎖):
1)若是是Serializable,讀操做走寫流程,會申請StrongRead鎖
2)若是是snapshot isolation,讀操做不申請任何鎖。
3)若是是Serializable,UpSert操做申請StrongWrite鎖,其餘寫操 做 (insert/update/delete)申請StrongRead + StrongWrite鎖
4)若是是snapshot isolation,申請 StrongRead + StrongWrite鎖
5)全部父路徑都申請對應的weak鎖。
目前看來,yugabyte把不一樣隔離級別的事務衝突語義所有在該階段完成,保證只要這裏放行的事務,後續的併發執行不會出現問題,全部可能引發數據正確性不符合隔離級別的事務要麼在該階段被取消,要麼把其餘正在執行的事務取消,讓本身得以執行。

數據複製階段\

這一階段經過raft將對數據庫的修改操做複製到全部副本所在節點上,當獲得大多數節點完成複製的響應後,Leader會將數據寫入到rocksdb(對於分佈式事務,寫intent_db,記錄中包含鎖),而後leader會釋放內存鎖。

7.2.4 TransactionStatus::COMMITTED

// 收到commit消息後,會檢查事務是否被其餘事務給取消了
// raft將TransactionStatus::COMMITTED消息複製到全部副本,複製完成後,更新 commit_time_  
commit_time_ = data.hybrid_time; // 這個時間戳是 COMMITTED這rpc請求對應的operation在start階段從MvccManager申請的
// 而後調用StartApply將 APPLYING請求入隊,該請求的參數以下:
  void StartApply() {
    if (context_.leader()) {
      for (const auto& tablet : involved_tablets_) {  // 通知當前事務的全部涉及到的tablet,執行APPLY操做
        context_.NotifyApplying({
            .tablet = tablet.first,
            .transaction = id_,
            .commit_time = commit_time_,  //  commit_time就是 COMMITTED消息被raft複製完成後的時間戳
            .sealed = status_ == TransactionStatus::SEALED});
      }
    }
  }
複製代碼

7.2.5 TransactionStatus::APPLYING

//  COMMITTED 消息處理完後,對發起事務的客戶端來講,事務的流程已經走完了,數據已經可見了。而對yugabyte來講,
// 數據還停留在intent_db中,須要移動到normal_db,並設置LocalCommitedTime,供其餘併發事務檢測衝突使用。
// 該消息是由TransactionParticipant處理的,一樣的會先將該消息經過raft複製到tablet的全部副本,raft複製完該消息後,
// 執行ProcessApply,將數據從intent_db 遷移到 normal_db,而後註冊異步任務刪除intent_db中的數據
HybridTime commit_time(data.state.commit_hybrid_time());  // 這是 COMMITTED的時間戳
TransactionApplyData apply_data = {
        data.leader_term, 
        id,                 // 事務ID
        data.op_id,         // operation id
        commit_time,        //  COMMITTED的時間戳
        data.hybrid_time,     //  APPLYING的時間戳
        data.sealed,
        data.state.tablets(0) };
ProcessApply(apply_data);


// 每一個tablet上的  ProcessApply 工做流程(目前只有局部tablet視角)
CHECKED_STATUS ProcessApply(const TransactionApplyData& data) {
    {  // 一頓操做,主要是設置 Local Commit Time

      // It is our last chance to load transaction metadata, if missing.
      // Because it will be deleted when intents are applied.
      // We are not trying to cleanup intents here because we don't know whether this transaction
      // has intents of not.
      auto lock_and_iterator = LockAndFind(
          data.transaction_id, "pre apply"s, TransactionLoadFlags{TransactionLoadFlag::kMustExist});
      if (!lock_and_iterator.found()) {
        // This situation is normal and could be caused by 2 scenarios:
        // 1) Write batch failed, but originator doesn't know that.
        // 2) Failed to notify status tablet that we applied transaction.
        LOG_WITH_PREFIX(WARNING) << Format("Apply of unknown transaction: $0", data);
        NotifyApplied(data);
        CHECK(!FLAGS_fail_in_apply_if_no_metadata);
        return Status::OK();
      }


      lock_and_iterator.transaction().SetLocalCommitTime(data.commit_ht);


      LOG_IF_WITH_PREFIX(DFATAL, data.log_ht < last_safe_time_)
          << "Apply transaction before last safe time " << data.transaction_id
          << ": " << data.log_ht << " vs " << last_safe_time_;
    }


    // 經過事務反向索引,找到全部的intent,而後apply到normal_db

    CHECK_OK(applier_.ApplyIntents(data));


    {// 這裏發起異步刪除 intent 任務(刪除intent_db中的數據)
      MinRunningNotifier min_running_notifier(&applier_);
      // We are not trying to cleanup intents here because we don't know whether this transaction
      // has intents or not.
      auto lock_and_iterator = LockAndFind(
          data.transaction_id, "apply"s, TransactionLoadFlags{TransactionLoadFlag::kMustExist});
      if (lock_and_iterator.found()) {
        RemoveUnlocked(lock_and_iterator.iterator, "applied"s, &min_running_notifier);
      }
    }


    NotifyApplied(data);
    return Status::OK();
  }
複製代碼

7.2.6 TransactionStatus::ABORTED and TransactionStatus::CLEANUP

事務由於某些緣由(發生錯誤或用戶主動abort)走到abort流程後,須要分別由coordinator變動status tablet狀態,由participate清理垃圾數據。

相關文章
相關標籤/搜索