HBase 事務和併發控制機制原理

位元科技發表於2016-02-27

作為一款優秀的非記憶體資料庫,HBase和傳統資料庫一樣提供了事務的概念,只是HBase的事務是行級事務,可以保證行級資料的原子性、一致性、隔離性以及永續性,即通常所說的ACID特性。為了實現事務特性,HBase採用了各種併發控制策略,包括各種鎖機制、MVCC機制等。本文首先介紹HBase的兩種基於鎖實現的同步機制,再分別詳細介紹行鎖的實現以及各種讀寫鎖的應用場景,最後重點介紹MVCC機制的實現策略。

HBase同步機制

HBase提供了兩種同步機制,一種是基於CountDownLatch實現的互斥鎖,常見的使用場景是行資料更新時所持的行鎖。另一種是基於ReentrantReadWriteLock實現的讀寫鎖,該鎖可以給臨界資源加上read-lock或者write-lock。其中read-lock允許併發的讀取操作,而write-lock是完全的互斥操作。

CountDownLatch

Java中,CountDownLatch是一個同步輔助類,在完成一組其他執行緒執行的操作之前,它允許一個或多個執行緒阻塞等待。CountDownLatch使用給定的計數初始化,核心的兩個方法是countDown()和await(),前者可以實現給定計數倒數一次,後者是等待計數倒數到0,如果沒有到達0,就一直阻塞等待。結合執行緒安全的map容器,基於test-and-set機制,CountDownLatch可以實現基本的互斥鎖,原理如下:

1. 初始化:CountDownLatch初始化計數為1

2. test過程:執行緒首先將臨界資源作為key,latch作為value嘗試插入執行緒安全的map中。如果返回失敗,表示其他執行緒已經持有了該鎖,呼叫await方法阻塞到該latch上,等待其他執行緒釋放鎖;

3. set過程:如果返回成功,就表示已經持有該鎖,其他執行緒必然插入失敗。持有該鎖之後執行各種操作,執行完成之後釋放鎖,釋放鎖首先將map中對應的KeyValue移除,再呼叫latch的countDown方法,該方法會將計數減1,變為0之後就會喚醒其他阻塞執行緒。

ReentrantReadWriteLock

讀寫鎖分為讀鎖、寫鎖,和互斥鎖相比可以提供更高的並行性。讀鎖允許多個執行緒同時以讀模式佔有鎖資源,而寫鎖只能由一個執行緒以寫模式佔有。如果讀寫鎖是寫加鎖狀態,在鎖釋放之前,所有試圖對該鎖佔有的執行緒都會被阻塞;如果是讀加鎖狀態,所有其他對該鎖的讀請求都會並行執行,但是寫請求會被阻塞。顯而易見,讀寫鎖適合於讀多寫少的場景,也因為讀鎖可以共享,寫鎖只能某個執行緒獨佔,讀寫鎖也被稱為共享-獨佔鎖,即經常見到的S鎖和X鎖。

Java中,ReentrantReadWriteLock是讀寫鎖的實現類,該類中有兩個方法readLock()和writeLock()分別用來獲取讀鎖和寫鎖。

HBase中行鎖的具體實現

HBase採用行鎖實現更新的原子性,要麼全部更新成功,要麼失敗。所有對HBase行級資料的更新操作,都需要首先獲取該行的行鎖,並且在更新完成之後釋放,等待其他執行緒獲取。因此,HBase中對同一行資料的更新操作都是序列操作。

行鎖相關資料結構

如上圖所示,HBase中行鎖相關的主要結構有RowLock和RowLockContext兩個類,其中RowLockContext類儲存行鎖相關上下文資訊,包括持鎖執行緒、被鎖物件以及可以實現互斥鎖的CountDownLatch物件等等,RowLockContext是RowLock的一個屬性,除此之外,RowLock還包含表徵行鎖是否已經釋放的release欄位。具體欄位如下圖所示:

更新加鎖流程

1. 首先使用rowkey以及自身執行緒物件生成行鎖上下文RowLockContext物件

2. 再將rowkey作為key,RowLockContext物件作為value呼叫putIfAbsert方法寫入全域性map中。key的唯一性,保證map中最多隻有一個RowLockContext。putIfAbsent方法會返回一個existingContext物件,該物件表示key插入前map中對應該key的value值,根據existingContext是否為null、是否是自身執行緒建立,可以分為如下三種情況:

(1)existingContext物件為null,表示該行鎖沒有被其他執行緒持有,可以根據建立的上下文物件持有該鎖
(2)existingContext是自身執行緒建立,表示自身執行緒已經再建立RowLockContext物件,直接使用存在的RowLockContext物件持有該鎖。這種情況會出現在批量更新執行緒中,一次批量更新可能前前後後對某一行資料更新多次,需要多次持有該行資料的行鎖,在HBase中是被允許的。
(3)existingContext是其他執行緒建立,則該執行緒會阻塞在此上下文所持鎖上,直至所持行鎖被釋放或者阻塞超時。如果所持行鎖釋放,該執行緒會重新競爭寫全域性map,一旦競爭成功就持有該行鎖,否則繼續阻塞。而如果阻塞超時,就會丟擲異常,不會再去競爭該鎖。

釋放流程

線上程更新完成操作之後,必須在finnally方法中執行行鎖釋放操作,即呼叫rowLock.release()方法,該方法主要執行如下兩個操作:

1. 從lockedRows這個全域性map中將該row對應的RowLockContext移除

2. 呼叫latch.countDown()方法,喚醒其他阻塞在await上等待該行鎖的執行緒

HBase中讀寫鎖的使用

HBase中除了使用互斥鎖實現行級資料的一致性之外,也使用讀寫鎖實現store級別操作以及region級別操作的併發控制。比如:

1. Region更新讀寫鎖:HBase在執行資料更新操作之前都會加一把Region級別的讀鎖(共享鎖),所有更新操作執行緒之間不會相互阻塞;然而,HBase在將memstore資料落盤時會加一把Region級別的寫鎖(獨佔鎖)。因此,在memstore資料落盤時,資料更新操作執行緒(Put操作、Append操作、Delete操作)都會阻塞等待至該寫鎖釋放。

2. Region Close保護鎖:HBase在執行close操作以及split操作時會首先加一把Region級別的寫鎖(獨佔鎖),阻塞對region的其他操作,比如compact操作、flush操作以及其他更新操作,這些操作都會持有一把讀鎖(共享鎖)

3. Store snapshot保護鎖:HBase在執行flush memstore的過程中首先會基於memstore做snapshot,這個階段會加一把store級別的寫鎖(獨佔鎖),用以阻塞其他執行緒對該memstore的各種更新操作;清除snapshot時也相同,會加一把寫鎖阻塞其他對該memstore的更新操作。

HBase中MVCC機制的實現

如上文所述,HBase分別提供了行鎖和讀寫鎖來實現行級資料、Store級別以及Region級別的併發控制。除此之外,HBase還提供了MVCC機制實現資料的讀寫併發控制。MVCC,即多版本併發控制技術,它使得事務引擎不再單純地使用行鎖實現資料讀寫的併發控制,取而代之的是,把行鎖與行的多個版本結合起來,經過簡單的演算法就可以實現非鎖定讀,進而大大的提高系統的併發效能。HBase正是使用行鎖 + MVCC保證高效的併發讀寫以及讀寫資料一致性。

MVCC機制簡介

在瞭解HBase如何實現MVCC之前,我們首先需要了解當前僅基於行鎖實現的更新操作對於讀請求有什麼影響。下圖為HBase基於行鎖實現的資料更新時序示意圖:

上圖中簡單地表述了資料更新流程(後續文章會對HBase資料寫入進行深入的介紹),簡單來說,資料更新可以分為如下幾個階段:獲取行鎖、更新WAL、資料寫入本地快取memstore、釋放行鎖。

如上圖所示,前後分別有兩次對同一行資料的更新操作。假如第二次更新過程在將列簇cf1更新為t2_cf1之後中有一次讀請求進來,此時讀到的第一列資料將是第二次更新後的資料t2_cf1,然而第二列資料卻是第一次更新後的資料t1_cf2,很顯然,只針對更行操作加行鎖會產生讀取資料不一致的情況。最簡單的資料不一致解決方案是讀寫執行緒公用一把行鎖,這樣可以保證讀寫之間互斥,但是讀寫執行緒同時搶佔行鎖必然會極大地影響效能。

為此,HBase採用MVCC解決方案避免讀執行緒去獲取行鎖。MVCC解決方案對上述資料更新操作時序和讀操作都進行了一定的修正,主要新增了一個寫序號和讀序號,其實就是資料的版本號。修正後的更新操作時序示意圖為:

如上圖所示,修正後的更新操作主要新增了‘獲取寫序號’和’結束寫序號’兩個步驟,並且每個cell資料寫memstore操作都會攜帶該寫序號。那讀請求需要經過什麼樣的修正呢?HBase的做法如下:

(1)每個讀操作開始時都會分配一個讀序號,稱為讀取點
(2)讀取點的值是所有的寫操作完成序號中的最大整數
(3)一次讀操作的結果就是讀取點對應的所有cell值的集合

如下圖所示,第一次更新獲取的寫序號為1,第二次更新獲取的寫序號為2。讀請求進來時寫操作完成序號中的最大整數為wn = 1,因此對應的讀取點為wn = 1,讀取的結果為wn = 1所對應的所有cell值集合,即為t1_cf1和t1_cf2,這樣就可以實現以無鎖的方式讀取到一致的資料。

HBase中MVCC實現

HBase中,MVCC的具體實現類為MultiVersionConsistencyControl,該類維護了兩個long型的變數、一個WriteEntry物件和一個writeQueue佇列:

1. long memstoreRead:記錄當前全域性的讀取點,讀請求進來之後首先會獲取該讀取點

2. long memstoreWrite:記錄當前全域性的寫序號,根據它為下一個更新執行緒分配新的寫序號

3. writeEntry:記錄更新操作的寫序號物件,主要包含兩個變數,一個是writeNumber,表示寫序號;一個是布林型別的completed,表示該次更新是否完成

4. writeQueue:當前所有更新操作的寫序號物件集合

獲取寫序號

根據上文中更新資料時序圖可知,更新執行緒獲取行鎖之後就需要獲取寫序號,對應的方法為beginMemstoreInsert,該方法將memstoreWrite加1,生成writeEntry物件並插入到佇列writeQueue,返回writeEntry物件。Note:生成的writeEntry物件中包含寫序號writeNumber,更新執行緒會將該writeNumber設定為cell資料的一個屬性。

結束寫序號

資料更新完成之後,釋放行鎖之前,更新執行緒會呼叫completeMemstoreInsert方法更新writeEntry物件以及memstoreRead變數,具體分為如下兩步:

1. 首先將該writeEntry物件標記為’已完成’,再將全域性讀取點memstoreRead儘可能多地往前移。前移演算法為遍歷佇列writeQueue中所有的writeEntry物件,移除掉已經標記為’已完成’的writeEntry直至遇到未完成的writeEntry,最後將memstoreRead變數更新為最新已完成的writeNumber。

2. 注意上述memstoreRead變數有可能並不等於當前更新執行緒的writeNumber,這種情況下該更新執行緒對資料的更新操作對使用者並不可見。為了實現更新完成之後更新結果即對使用者可見,需要等待memstoreRead變數前移到當前更新執行緒的witeNumber。因此它會阻塞當前執行緒,等待其他執行緒對應的writeEntry物件標記為’已完成’,直至memstoreRead等於當前執行緒的writeNumber。

總結

HBase提供了各種鎖機制和MVCC機制來保證資料的原子性、一致性等特性,其中使用互斥鎖實現的行鎖保證了行級資料的原子性,使用JDK提供的讀寫鎖實現了Store級別、Region級別的資料一致性,同時使用行鎖+MVCC機制實現了在高效能非鎖定讀場景下的資料一致性。

相關文章