菜鳥系列Fabric原始碼學習 — MVCC驗證
Fabric 1.4 原始碼分析 MVCC驗證
讀本節文件之前建議先檢視[Fabric 1.4 原始碼分析 committer記賬節點]章節。
1. MVCC簡介
Multi-Version Concurrency Control 多版本併發控制,MVCC 是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問。在資料庫系統中,鎖機制可以控制併發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。MVCC是通過儲存資料在某個時間點的快照來實現的. 不同儲存引擎的MVCC. 不同儲存引擎的MVCC實現是不同的,典型的有樂觀併發控制和悲觀併發控制.
2. MVCC樣例介紹
InnoDB的MVCC,是通過在每行記錄後面儲存兩個隱藏的列來實現的,這兩個列,分別儲存了這個行的建立時間,一個儲存的是行的刪除時間。這裡儲存的並不是實際的時間值,而是系統版本號(可以理解為事務的ID),每開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會作為事務的ID。其中MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作。
SELECT
InnoDB會根據以下兩個條件檢查每行紀錄:
InnoDB只查詢版本早於當前事務版本的資料行,即,==行的系統版本號小於或等於事務的系統版本號==,這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
==行的刪除版本,要麼未定義,要麼大於當前事務版本號==。這樣可以確保事務讀取到的行,在事務開始之前未被刪除。
只有符合上述兩個條件的紀錄,才能作為查詢結果返回。- INSERT
InnoDB為插入的每一行儲存當前系統版本號作為行版本號。 - DELETE
InnoDB為刪除的每一行儲存當前系統版本號作為行刪除標識。 UPDATE
InnoDB為插入一行新紀錄,儲存當前系統版本號作為行版本號,同時,儲存當前系統版本號到原來的行作為行刪除標識。
優點:
儲存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀資料操作很簡單,效能很好。
缺點:
每行紀錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作
3. Fabric裡面MVCC的實現
這裡回顧幾個知識點:
- 狀態由鍵值對組成。所有鍵值條目都是帶有版本的
- 鍵的版本只記錄在讀集中;寫集只包含鍵和交易設定的鍵的最新值
- 使用讀寫集中的讀集來驗證交易,使用寫集來更新受影響的鍵的版本和值
- 使用交易的高度來作為版本號
3.1 驗證公共資料讀集key
讀集中鍵的版本和世界狀態中鍵的版本一致就認為該交易是 合法的。
if !version.AreSame(committedVersion, rwsetutil.NewVersion(kvRead.Version)) {
return false, nil
}
版本資料結構
type Version struct {
BlockNum uint64
TxNum uint64
}
當驗證完一筆交易後,如果交易有效,會更新key版本,接著再驗證下一筆交易。
committingTxHeight := version.NewHeight(block.Num, uint64(tx.IndexInBlock))
updates.ApplyWriteSet(tx.RWSet, committingTxHeight, v.db)
在此舉例介紹,mycc鏈碼a轉賬給b。
例項化鏈碼交易在區塊3中,則a、b版本為
{
"key": "a",
"version": {
"block_num": "3",
"tx_num": "0"
}
}
發起一筆有效交易後,版本更新為
{
"key": "a",
"version": {
"block_num": "4",
"tx_num": "0"
}
}
3.2 驗證range-query
當讀寫集中包含一個或多個查詢資訊(query-info)時,需要執行額外的驗證。這種額外的驗證需要確保在根據查詢資訊獲得的結果的超集(多個範圍的合併)中沒有插入、刪除或者更新鍵。
"range_queries_info":
[
{
"end_key":"marble3",
"itr_exhausted":true,
"raw_reads":{
"kv_reads":[
{
"key":"marble1",
"version":{
"block_num":"8",
"tx_num":"0"
}
},
{
"key":"marble2",
"version":{
"block_num":"9",
"tx_num":"0"
}
}
]
},
"start_key":"marble1"
}
]
validate()方法會根據rangeQueryInfo是否包含了合法當梅克爾樹摘要物件返回不同當驗證方法。
if rangeQueryInfo.GetReadsMerkleHashes() != nil {
logger.Debug(`Hashing results are present in the range query info hence, initiating hashing based validation`)
// 暫時全域性搜尋只發現ReadsMerkleHashes讀,沒發現寫
validator = &rangeQueryHashValidator{}
} else {
logger.Debug(`Hashing results are not present in the range query info hence, initiating raw KVReads based validation`)
validator = &rangeQueryResultsValidator{}
}
因此,在此只介紹rangeQueryResultsValidator;該方法會對讀集key以及版本與查詢結果進行一一比較。一致則返回true。
func (v *rangeQueryResultsValidator) validate() (bool, error) {
rqResults := v.rqInfo.GetRawReads().GetKvReads()
for i := 0; i < len(rqResults); i++ {
versionedKV := result.(*statedb.VersionedKV)
// versionedKV key驗證
if versionedKV.Key != kvRead.Key {
logger.Debugf("key name mismatch: Key in rwset = [%s], key in query results = [%s]", kvRead.Key, versionedKV.Key)
return false, nil
}
// versionedKV版本驗證
if !version.AreSame(versionedKV.Version, convertToVersionHeight(kvRead.Version)) {
logger.Debugf(`Version mismatch for key [%s]: Version in rwset = [%#v], latest version = [%#v]`,
versionedKV.Key, versionedKV.Version, kvRead.Version)
return false, nil
}
if result, err = itr.Next(); err != nil {
return false, err
}
}
}
3.3 驗證私密資料kvReadHash
當讀寫集中存在collection_hashed_rwset,需要驗證collHashedRWSet.HashedRwSet.HashedReads裡面的KVReadHash.Version
{
"collection_hashed_rwset":[
{
"collection_name":"collectionMarbles",
"hashed_rwset":"CiYKIF4flG/gcV3gNm0J6EgLrXZyojVRVwKbDd+8lYUPBFcOEgIIDw==",
"pvt_rwset_hash":null
}
],
"namespace":"marblesp",
"rwset":null
}
原始碼:
遍歷collHashedRWSets,再遍歷collHashedRWSet.HashedRwSet.HashedReads,最後對每個kvReadHash的版本進行驗證。
for _, collHashedRWSet := range collHashedRWSets {
if valid, err := v.validateCollHashedReadSet(ns, collHashedRWSet.CollectionName, collHashedRWSet.HashedRwSet.HashedReads, updates); !valid || err != nil {
return valid, err
}
}
for _, kvReadHash := range kvReadHashes {
if valid, err := v.validateKVReadHash(ns, coll, kvReadHash, updates); !valid || err != nil {
return valid, err
}
}
驗證程式碼與驗證key類似
committedVersion, err := v.db.GetKeyHashVersion(ns, coll, kvReadHash.KeyHash)
if err != nil {
return false, err
}
if !version.AreSame(committedVersion, rwsetutil.NewVersion(kvReadHash.Version)) {
logger.Debugf("Version mismatch for key hash [%s:%s:%#v]. Committed version = [%s], Version in hashedReadSet [%s]",
ns, coll, kvReadHash.KeyHash, committedVersion, kvReadHash.Version)
return false, nil
}
參考
- https://segmentfault.com/a/1190000012650596
- https://www.jianshu.com/p/db334404d909
- https://stone-fabric.readthedocs.io/zh/latest/readwrite.html