分布式事務實現-Percolator


  Google為了解決網頁索引的增量處理,以及維護數據表和索引表的一致性問題,基於BigTable實現了一個支持分布式事務的存儲系統。這里重點討論這個系統的分布式事務實現,不討論percolator中為了支持增量計算而實現的Notifications機制。

  該系統基於BigTable,支持snapshot isolation隔離級別,這個隔離級別不在ANSI定義的隔離級別范圍內。簡單來說,就是一個事務看到的是一個stable的數據庫的快照。快照隔離相對於可串行化隔離級別的優點是更高的讀性能,不需要加鎖,MVCC基於BigTable的多版本機制。缺點是有write skew問題,簡單來說,對於兩個事務T1:b=a+1和T2:a=b+1,初始化a=b=0。串行化的情況下,結果只可能是(a=2,b=1)或者(a=1,b=2),而在快照隔離級別下,結果可能是(a=1,b=1)。這在某些業務場景下是不能接受的。既然有多版本,就需要有版本號,percolator系統使用一個全局遞增時間戳服務器來為事務產生時間戳,每個事務開始時拿一個時間戳t1,那么這個事務執行過程中可以讀t1之前的數據。提交時再取一下時間戳t2,作為這個事務的提交時間戳。

  現在說分布式事務。說起分布式事務第一個想到的就是兩階段提交,這個系統也不例外。客戶端作為協調者coordinator,BigTable的tablet server作為參與者participant。 除了實際的表的每個Cell的數據存在BigTable中外,coordinator還將Cell鎖信息,事務版本號存在BigTable中。簡單來說,如果需要寫列C,在BigTable中實際存在三列,分別為C:data,C:lock,C:write。由於BigTable實際上定位一個Value需要三個信息,rowkey,column和timestamp,所以實際上一個 column本身內部可以看成一個timestamp->value的map。那么:

  1. c:write中存事務提交時間戳commit_ts=>start_ts。   

  2. c:data這個map中存事務開始時間戳start_ts=>實際列數據

  3. c:lock存start_ts=>(primary cell),primary cell是rowkey和列名的組合,它在兩階段提交容錯處理和事務沖突時使用,用來清理由於coordinator失敗導致的分布式事務失敗留下的鎖信息。

      舉個沒有任何沖突例子,假設一個分布式事務T1需要修改兩個Cell,C1(Rowkey1:C1)和C2(Rowkey2:C2),C1為primary cell,Value分別為Value1和Value2,並且兩個Cell處於不同的tablet server,serverA和serverB。客戶端commit之前首先將兩個Cell都加入到客戶端本地的一個數組中,最后事務commit(包括兩階段的prepare和commit)的時候才將所有Cell發向tablet server。

沒有檢測到沖突的寫事務流程:

      prepare階段:

      1. 分布式事務T1啟動,從全局時間戳服務器獲取事務啟動時間戳記作t1_start_ts。

      2. 首先寫primary cell C1,往C1:data中寫入t1_start_ts=>value1,往C1:lock中寫入t1_start_ts=>primary cell 表示加鎖,同理,寫serverB,往C2:data中寫入t1_start_ts=>value2, 往C2:lock中寫入t1_start_ts=>primary cell

      commit階段:

      1. 從全局時間戳服務器獲取事務提交時間戳記作t1_commit_ts。 

      2. 啟動一個C1所在的BigTable行事務,包含以下兩個操作

    2.1 往primary cell C1:write寫入t1_commit_ts=>t1_start_ts(這步是關鍵)

    2.2 將primary cell的lock release(delete C1:lock,時間戳為t1_start_ts)

      3. Commit這個BigTable 事務,這一步實際上將這個事務的數據對外可見,因為隨后的一個讀事務(事務啟動時間戳記作t2_start_ts)讀C1之前,會首先讀C1:write的小於t2_start_ts的最大的版本的數據獲得t1_start_ts,然后拿着t1_start_ts才能去C1:data中讀取真正的數據。

      4. 將其他secondary cell C2:write中寫入t1_commit_ts=>t1_start_ts,release C2的lock

 沒有檢測到沖突的讀C1和C2的事務T3流程:

  1. 從全局時間戳服務器獲取事務提交時間戳記作t3_start_ts

  2. 分別讀C1和C2,讀C:write的比t3_start_ts小的最大的一個事務提交時間戳的事務啟動時間,然后拿這個事務啟動時間去C:data中讀真正的數據。

可以看出,一個Cell對外可見是通過寫C:write來達到的,t1_commit_ts為事務提交版本號,t1_start_ts為t1這個事務修改后的數據版本號,真正讀數據需要拿到t1_start_ts,而讀t1_start_ts又需要首先拿到t1_commit_ts。

   協調者(Client)宕機容錯

   假設C1上鎖失敗,C2上鎖成功,那么分布式事務失敗會將C2的鎖殘留在BigTable中。這個殘留的鎖由后續第一個讀或寫C2的事務來清除,滿足什么樣的條件才能清除?滿足以下兩個條件中的一個即可:1. 寫這個lock的客戶端在chubby上的結點沒了,即客戶端死了 2. C2:lock這把鎖滯留時間太長了(lock內部保存最后更新時間即可)。  cleanup的操作就是直接delete C2:lock即可,時間戳為t2_start_ts_(percolator論文中此處有筆誤)。但是如何知道殘留下這個鎖的事務是否已經提交?這就需要去讀C2的primary cell的write字段,在這個例子里就是讀C1:write,記殘留下來的鎖C2:lock的時間戳為lock_ts(percolator論文這里說的不清楚),那么具體的判斷事務是否提交的操作就是讀取C1:write的[lock_ts,正無窮)的所有版本,判斷是否有一個版本的值是lock_ts,如果有,則說明殘留鎖的事務已經提交。    

    事務沖突

    Prepare階段沖突:寫C1之前需要首先讀取C1:lock,如果有任何一個版本被加上了鎖,那么這次分布式事務失敗。還有一種沖突是,有其他事務在本事務開始之后commit修改了C1,從而修改了C1:write,這是一種Snapshot isolation需要避免的寫寫沖突。

    Commit階段沖突:分布式事務提交需要先提交primary cell,再提交其他cell,再提交primary cell時需要先檢查自己是否還拿住了primary cell的鎖,在這里是C1:lock,即t1_start_ts_版本是否已經被刪除。做這個判斷的原因是其他事務可以cleanup這個lock,如果它認為這個事務握有鎖時間過長或者寫入lock的客戶端宕機太慢等原因。在這里,primary cell的lock字段是其他事務進行cleanup操作和當前事務提交操作的同步點。

 

參考資料:

http://static.googleusercontent.com/media/research.google.com/en/us/pubs/archive/36726.pdf

https://github.com/XiaoMi/themis/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM