Redis學習筆記&源碼閱讀--字典-概念

申明

  • 本文基於Redis源碼5.0.8
  • 本文內容大量借鑑《Redis設計和實現》和《Redis5設計與源碼分析》

概念

字典主要是用來存儲健值對的一種數據結構,詳細的概念不贅述了,我相信你不是爲了釋義來看我的博客。在Redis中字典有如下特徵:

  1. 可以存儲海量數據,鍵值對是映射關係,可以根據鍵以O(1)的時間複雜度取出或插入關聯值。
  2. 鍵值對中鍵的類型可以是字符串、整型、浮點型等,且鍵是唯一的。
  3. 鍵值對中值的類型可爲String、Hash、List、Set、SortedSet。

《Redis5設計與源碼分析》一書中對字典的核心組成進行了比較詳細的講解,大家有條件的建議去看看,我這裏只是嘗試簡化梳理下。

能夠實現O(1)時間複雜度去取值的,首先想到的是數組,即使是存儲海量數據也可以按照下標以O(1)時間複雜度取值,滿足特徵1,此時一個字典的結構如下圖所示:

但是數組的訪問是以下標,而特徵2中健的類型可以是字符串等其他類型,這個時候就需要對健做一些特殊操作,處理的過程我們稱之爲Hash。

Hash函數

Hash的作用是把任意長度的輸入通過算法轉換成固定類型、固定長度的值,相同的輸入經過Hash計算後得到相同的輸出,不同的輸入經過Hash後一般情況下得出的都是不一樣的輸出,小概率情況下會得到相同的輸出(發生碰撞)。
這個時候我們可以將健通過Hash函數轉換成整數類型的值了,但是Hash函數計算後的整數非常大,我們不能直接將其作爲下標使用,那怎麼辦呢?最簡單的方法就是對數組的容量求餘,使用餘作爲下標值訪問數組,此時我們就需要引入一個數組容量的字段到字典中,既然有容量,那麼每次插入前是不是要判斷當前容量是否足夠?還需要引入一個當前使用量字段,此時字典的結構如下圖所示:
在這裏插入圖片描述
這個方法不是完美的,因爲可能出現不同的值求餘結果一樣,這種情況和剛纔說的使用Hash計算出相同的結果統稱爲Hash衝突。

Hash衝突

爲了處理Hash衝突,數組中的元素除了保存實際的值以外還要保存鍵的值和next指針用於指向衝突的下一個鍵值對,next指針可以把衝突的鍵值對串成單鏈表,“鍵”信息用於判斷是否爲當前要查找的鍵。此時字典的機構如下圖所示:
在這裏插入圖片描述
爲了保證鍵是唯一的,可以在代碼中保證,每次插入時都先查詢一次,這樣特徵2就實現了,特徵3的實現比較簡單,可以將字典中的具體值改爲一個指針,指向任意內存。
經過上面的講解,對字典的構成應該有了一個初步的認識,那麼接下來我們看看在Redis中字典是怎麼被實現的。

Redis中字典的實現

在講解Redis中字典的構成前,我希望大家銘記字典的結構分層,這有助加速你瞭解字典,那就是
字典,Hash表,鏈表!!!
字典,Hash表,鏈表!!!
字典,Hash表,鏈表!!!

在我們介紹各個功能時,應該在腦海中自己思考這屬於哪一層,我是這麼做的,並且覺得很有幫助,希望對你也如此。
這裏不按照由外向裏的邏輯逐步深入Redis結構,而是直接先介紹字典的核心Hash表,再瞭解封裝了Hash表的字典和Hash表維護的元素鏈表。

Hash表

Redis源碼中Hash表的結構如下所示:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table; //指針數組,保存kv數據
    unsigned long size;//table的size,主要不是table中保存kv對的個數,table是一個數組,size是table數組的長度
    unsigned long sizemask;//掩碼,其值永遠等於size-1,用於在計算hash值後進行位運算計算新元素保存在table數組的哪些元素內(table保存的實際是一個鏈表)
    unsigned long used;//整個table保存了多少個kv對,主要用於擴縮容時使用
} dictht;

table是一個指針數組,數組中每一個元素保存的是一個指向entry的指針,entry中包括具體的key和value,也包括了一個指向了下一個元素的next指針,所以我們將table的元素在概念上理解爲一個鏈表是比較合適的。一個entry具體的格式後面會介紹。
size指的是table數組的長度,sizemask是掩碼,其值永遠都是size-1,爲什麼要設計這個變量呢?我們知道計算一個新entry在table中保存的具體位置,是先使用Hash函數計算出一個值,然後對size求餘,但是求餘運算實際上是相對比較消耗資源的,Redis爲了提高性能是做了特殊的優化。我們以實際例子來看,假設table的size是4,sizemask就是3,其二進制表示就是11,然後Hash值和11的與運算結果就是Hash值對size求餘的結果。Redis就是利用了size爲2^n時的這種特性來優化求餘運算的,所以在Redis中table的size只會是4,8,16,32,64,128…這種,sizemask對應爲3,7,15,63,127,對應的二進制是11,111,1111,11111…。其實這種操作是很常見的。
used表示的是table中所有鏈表保存元素的數量,其作用是在計算字典是否需要擴縮容時使用的。

字典

講完了字典的核心數據結構,我們再來看看封裝了Hash表的字典是什麼結構的,先看它的源碼:

typedef struct dict {
    dictType *type;//字典保存元素特定的操作函數集合
    void *privdata;//type中函數使用到的數據
    dictht ht[2];//Hash表數組
    long rehashidx;//默認值-1,表示字典當前不在rehash,否則表示字典正在進行rehash操作,值表示ht[0]中rehash執行到了該下標
    unsigned long iterators; //當前字典中執行的迭代器數量
} dict;

type字段是dictType類型的,先看下它的真身:

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);//字典對應的Hash函數
    void *(*keyDup)(void *privdata, const void *key);//鍵的複製函數
    void *(*valDup)(void *privdata, const void *obj);//值得複製函數
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);//鍵的比較函數
    void (*keyDestructor)(void *privdata, void *key);//鍵的析構函數
    void (*valDestructor)(void *privdata, void *obj);//值得析構函數
} dictType

將這些函數封裝在dictType並提供給用戶配置,就實現了字典保存不同類型數據的多態屬性。字典中的privdata是配合這type一起使用的數據。
ht是包含了兩個Hash表的數組,我們一般情況下只會使用到ht[0]的,只有當字典因爲擴容或者縮容需要做rehash時纔會短暫時間內使用,當擴容或者縮容結束後,新的Hash賦值給ht[0],ht[1]就不需要使用了。
rehashidx用來標記該字典是否在進行rehash,沒進行rehash時,值爲-1,否則,該值用來表示Hash表ht[0] 執行rehash到了哪個元素,並記錄該元素的數組下標值。
iterators字段,用來記錄當前運行的安全迭代器數,當有安全迭代器綁定到該字典時,會暫停rehash操作。Redis很多場景下都會用到迭代器,例如:執行keys命令會創建一個安全迭代器,此時iterators會加1,命令執行完畢則減1,而執行sort命令時會創建普通迭代器,該字段不會改變。

鏈表

其實講到這裏字典已經講完了,已經沒有新鮮的內容出爐了,提鏈表實際是爲了提醒你Hash表中table這個指針的指針本質是一個一維數組,數組的每個元素是一個鏈表,牢牢的記住這一點有助於我們理解Redis對字典的各項操作。

最後我們看下一個完整字典的結構:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章