1. 程式人生 > >菜鳥系列Fabric原始碼學習 — MVCC驗證

菜鳥系列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
}

參考

  1. https://segmentfault.com/a/1190000012650596
  2. https://www.jianshu.com/p/db334404d909
  3. https://stone-fabric.readthedocs.io/zh/latest/readwrite.html