本文轉載自:http://www.cnblogs.com/panfeng412/p/mvcc-implementation-mechanism-in-hbase.html
MVCC(Multi-Version Concurrent Control),即多版本併發控制協議,廣泛使用於數據庫系統。本文將介紹HBase中對於MVCC的實現及應用情況。
MVCC基本原理
在介紹MVCC概念之前,我們先來想一下數據庫系統裏的一個問題:假設有多個用戶同時讀寫數據庫裏的一行記錄,那麼怎麼保證數據的一致性呢?一個基本的解決方法是對這一行記錄加上一把鎖,將不同用戶對同一行記錄的讀寫操作完全串行化執行,由於同一時刻只有一個用戶在操作,因此一致性不存在問題。但是,它存在明顯的性能問題:讀會阻塞寫,寫也會阻塞讀,整個數據庫系統的併發性能將大打折扣。
MVCC(Multi-Version Concurrent Control),即多版本併發控制協議,它的目標是在保證數據一致性的前提下,提供一種高併發的訪問性能。在MVCC協議中,每個用戶在連接數據庫時看到的是一個具有一致性狀態的鏡像,每個事務在提交到數據庫之前對其他用戶均是不可見的。當事務需要更新數據時,不會直接覆蓋以前的數據,而是生成一個新的版本的數據,因此一條數據會有多個版本存儲,但是同一時刻只有最新的版本號是有效的。因此,讀的時候就可以保證總是以當前時刻的版本的數據可以被讀到,不論這條數據後來是否被修改或刪除。
更多關於MVCC基本思想的介紹,參考Wikipedia。
一個MVCC實現類
見org.apache.hadoop.hbase.regionserver.MultiVersionConsistencyControl,用於控制Memstore中讀寫的一致性,其中維護兩個long型的變量:
1)memstoreRead:用於記錄當前全局可讀的readPoint,同時爲了每個客戶端讀請求能夠記錄自己發起請求時刻的readPoint,還有一個ThreadLocal的perThreadReadPoint變量,以及相關的set和get方法;
2)memstoreWrite:用於記錄當前全局最大的writePoint,根據它爲下個事務生成新的writePoint。
MultiVersionConsistencyControl中關鍵的實現方法如下:
1)WriteEntry beginMemstoreInsert():開始一個更新操作,將memstoreWrite加1,創建writeQueue並插入到writeQueue,並返回WriteEntry對象;
2)void completeMemstoreInsert(WriteEntry e):完成當前更新操作,將WriteEntry對象標記爲可讀,具體分兩步:
- boolean advanceMemstore(WriteEntry e):從頭開始遍歷writeQueue,移除所有已完成的WriteEntry對象,最後將memstoreRead更新爲最新已完成的memstoreWrite;
- void waitForRead(WriteEntry e):阻塞當前線程,直到memstoreRead等於當前WriteEntry的memstoreWrite,至此表明當前WriteEntry之前的所有更新事務都已經完成。
MVCC使用場景
見org.apache.hadoop.hbase.regionserver.HRegion.java,每個Region包含一個Memstore,維護一個MultiVersionConsistencyControl對象。
寫操作
見HRegion.java中的以下寫操作的方法:
1)put
2)checkAndPut
3)delete
4)checkAndDelete
5)internalFlushcache
6)mutateRow
7)mutateRowsWithLocks
8)batchMutate
最終會調用到applyFamilyMapToMemstore方法使用MVCC進行寫操作:
/** * Atomically apply the given map of family->edits to the memstore. * This handles the consistency control on its own, but the caller * should already have locked updatesLock.readLock(). This also does * <b>not</b> check the families for validity. * * @param familyMap Map of kvs per family * @param localizedWriteEntry The WriteEntry of the MVCC for this transaction. * If null, then this method internally creates a mvcc transaction. * @return the additional memory usage of the memstore caused by the * new entries. */ private long applyFamilyMapToMemstore(Map<byte[], List<KeyValue>> familyMap, MultiVersionConsistencyControl.WriteEntry localizedWriteEntry) { long size = 0; boolean freemvcc = false; try { if (localizedWriteEntry == null) { localizedWriteEntry = mvcc.beginMemstoreInsert(); freemvcc = true; } for (Map.Entry<byte[], List<KeyValue>> e : familyMap.entrySet()) { byte[] family = e.getKey(); List<KeyValue> edits = e.getValue(); Store store = getStore(family); for (KeyValue kv: edits) { kv.setMemstoreTS(localizedWriteEntry.getWriteNumber()); size += store.add(kv); } } } finally { if (freemvcc) { mvcc.completeMemstoreInsert(localizedWriteEntry); } } return size; }
讀操作
HRegion.java中通過private ConcurrentHashMap<RegionScanner, Long> scannerReadPoints;維護各個查詢請求的readPoint。
以get或scan請求爲例,最終會通過getScanner方法需要構造RegionScannerImpl對象:
org.apache.hadoop.hbase.regionserver.HRegion.RegionScannerImpl:
1)根據Scan對象構造時設置好readPoint,scan.getIsolationLevel()分爲READ_UNCOMMITTED和READ_COMMITTED,只有當READ_COMMITTED時根據MultiVersionConsistencyControl.resetThreadReadPoint(mvcc);設置當前scanner線程的readPoint,並插入到scannerReadPoints維護起來。
2)根據scan需要讀取的column family,創建StoreScanner(根據bloom filter、time range、ttl篩選需要的MemStoreScanner和StoreFileScanner),添加到scanners中,並最終根據scanners構造出一個KeyValueHeap。
下面看下RegionScannerImpl中的next方法是每次查詢時需要調用的函數:
boolean org.apache.hadoop.hbase.regionserver.HRegion.RegionScannerImpl.next(List<KeyValue> outResults, int limit) throws IOException
而上述方法會通過KeyValueHeap的next方法讀取下一條數據:先定位到當前KeyValueScanner(即之前構造KeyValueHeap時傳入的MemStoreScanner或StoreScanner),然後調用next方法。
StoreFileScanner和MemStoreScanner均爲KeyValueScanner,通過其中的next()接口方法,分別調用到StoreFileScanner.java的skipKVsNewerThanReadpoint方法、Memstore.java中MemStoreScanner對象的getNext方法。
1)StoreFileScanner.java的skipKVsNewerThanReadpoint方法:
2) Memstore.java中MemStoreScanner對象的getNext方法:
protected KeyValue getNext(Iterator<KeyValue> it) { long readPoint = MultiVersionConsistencyControl.getThreadReadPoint(); while (it.hasNext()) { KeyValue v = it.next(); if (v.getMemstoreTS() <= readPoint) { return v; } } return null; }