【Redis基本數據結構】字典實現 rehash介紹

字典, 又稱爲符號表 關聯數組或者映射,是一種保存鍵值對的抽象數據結構.
字典做爲一種經常使用數據結構被內置在許多程序語言中,因爲 C 語言沒有內置這種數據結構, Redis 構建了本身的字典實現.算法

字典在 Redis 中的應用至關普遍, 好比 Redis 的數據庫就是使用字典做爲底層實現的, 對數據庫的 增刪改查操做也是構建在對字典的操做之上的.數據庫

除了用做數據庫以外, 字典仍是哈希鍵的底層之一, 當一個哈希鍵包含的鍵值對較多,歐哲鍵值對中的元素都是比較長的字符串時, Redis 就會使用字典做爲哈希鍵的底層實現.編程

字典的實現

哈希表

Redis 字典所使用的哈希表由 dict.h/dictht 結構定義:數組

typedef struct dictht {
    dictEntry **table;      //哈希表數組
    unsigned long size;     //哈希表大小
    unsigned long sizemask; //用於計算索引值,
                            //老是等於 size - 1
    unsigned long used;     //哈希表已有節點數量
}  dictht;
  • table 屬性是一個數組, 數組中每一個元素都是一個指向 dictEntry 結構的指針, 每一個 dictEntry 結構保存着一個鍵值對.服務器

  • size 屬性記錄了哈希表的大小,也就是 table 數組的大小數據結構

  • sizemask 屬性和哈希值一塊兒決定一個鍵應該被放到 table 數組的哪一個索引上面ide

哈希表節點

哈希表節點使用 dictEntry 表示,每一個 dictEntry 結構保存着一個鍵值對函數

typedef struct dictEntry {
    void *key;          //鍵
    union {             //值
        void *val;
        uint_64 u64;
        int64_t s64;
    } v;                    
    sturct dictEntry *next; //指向下個哈希表節點,造成鏈表
} dictEntry;
  • 注意這裏 v 屬性保存着鍵值對中的值,其中的鍵值能夠是指針,或是 uint_64 整數,又或者是 int64_t 整數.性能

  • next 屬性是指向另外一個哈希表節點的指針,這個指針將多個哈希值相同的鍵值對鏈接在一塊兒,以此來解決鍵值衝突(collision)問題.ui

字典

Redis 中的字典 由 dict.h/dict 結構表示:

typedef struct dict {
    dictType *type;     //類型特定函數
    void *privdata;     //私有數據
    dictht ht[2];       //哈希表
    int rehashdx;      //rehash 索引,當 rehash 不在進行時,值爲-1
} dict;
  • type 和 privdata 是針對不一樣類型的鍵值對,爲建立多態字典而設置的

    `type`指向一個 `dictType` 結構的指針, 每一個` dictType` 結構保存了一簇用於操做特做特定類型鍵值對的函數, Redis 會爲用途不一樣的字典設置不一樣的類型特定函數.
    
    而` pridata` 則保存了須要傳給那些特定類型函數看可選參數.
  • ht 屬性,包含兩個數組,數組的每一項都是一個 dictht 哈希表,通常狀況下字典只使用ht[0] 哈希表,ht[1]哈希表只會在對哈希表進行 rehash 時使用.

  • rehashidex 記錄了 rehash 當前的進度,若是沒有進行 rehash, 值就爲-1.

下圖展現了一個普通狀態下的字典(沒有 rehash 進行)

哈希算法

當要將一個新的鍵值對添加到字典裏面時,程序會根據鍵計算出哈希值和索引值,而後再根據索引值,將包含新鍵值對的哈希表節點放到哈希表數組的指定索引上. 
Redis 計算哈希值和索引的方法以下:

hash = dict->type->hashFunction(k);
index = hash & dict->ht[0].sizemask

假設,要將上圖中鍵值對 k1和v1添加到字典中,使用 hashFunction 計算出 k1 的哈希值爲9,那麼

index = 9 & 3 = 1;

Redis 使用 MurmurHash2 算法 來計算鍵的哈希值.

解決鍵衝突

當有兩個或兩個以上的鍵被分配到了哈希表數組的同一索引上時,稱這些鍵發生了衝突( collision)

Redis 的哈希表使用鏈地址法來解決衝突,每一個哈希表節點都有一個 next 指針,多個哈希表節點能夠用 next 指針構成一個單項鍊表,被分配到同一個索引上的多個節點能夠用這個對單向鏈表鏈接起來,這就解決了鍵衝突的問題.

如前面的字典示意圖所示, 鍵 k0 和 k1 的索引值均爲1,這裏只需用 next 指針將兩個節點鏈接起來.
,由於dictEntry 節點組成的鏈表沒有表尾指針,爲了 速度考慮,程序老是將新節點調價到鏈表的表頭位置,排在其餘已有節點的前面,這樣插入的複雜度爲$ O(1)$.

Rehash

隨着操做的不斷進行, 哈希表保存的鍵值對會逐漸地增多或減小,爲了讓哈希表的負載因子維持在一個合理的範圍以內,當哈希表保存的鍵值對數量太多或者太少時, 程序須要對哈希表的大小進行相應的擴展或者收縮.這個過程叫作rehash.

Redis 對字典的哈希表執行 rehash 的步驟以下:

  1. 爲字典的 ht[1] 哈希表分配空間,空間的大小取決於要執行的操做,以及 ht0]當前包含的鍵值對數量( used 屬性值):

    • 若是執行的是擴展操做,那麼 ht[1] 的大小爲第一個大於等於 ht0].used*2的 $2^n$ .

    • 若是執行的是收縮操做,那麼ht[1]的大小爲打一個大於等於 ht[0].used 的$2^n$.

  2. 將保存在 ht[0] 中全部鍵值對 rehash 到 ht[1] 上面: 任何事指的是從新計算鍵的哈希值和索引值,而後鍵鍵值對放到 ht[1] 哈希表的指定位置.

  3. 當 ht[0] 包含的全部鍵值對都遷移到了 ht[1] 以後, 釋放 ht[0], 再將 ht[1] 設置爲 ht[0],並在 ht[1] 後面建立一個空白的哈希表.

舉個例子,假設程序要對下圖的 `ht[0] 進行擴展操做

  • ht[0].used 當前值爲4 , $2^3$ 剛好是第一個大於等於 4*2 的值,因此 ht[1] 哈希表的大小設置爲8,下圖展現了 ht[1] 分配了空間以後字典的樣子.

  • 將 ht[0] 包含的四個鍵值對 rehash 到 ht[1], 圖下圖所示:

  • 釋放 ht[0], 將 ht[1] 設置爲 ht[0]. 再分配一個空哈希表. 哈希表的大小由原來的4 擴展至8.

漸進式 rehash

上一節說過, 擴展或收縮哈希表須要將 ht[0] 裏的全部鍵值對 rehash 到 ht[1] 中,可是這個 rehash
動做並非一次性,集中式完成的,而是分屢次,漸進式完成的.

這麼作的緣由是,當哈希表裏保存的鍵值對多至百萬甚至億級別時,一次性地所有 rehash 的話,龐大的計算量會對服務器性能形成嚴重影響.

如下是漸進式 rehash 的步驟:

  1. 爲 ht[1] 分配空間

  2. 在字典中維持一個索引計數器變量 rehashidx, 將它的值設置爲0,表示 rehash 正式開始

  3. 在 rehash 進行期間,每次對字典進行增刪改查時,順帶將 ht[0] 在 rehashidx 索引上的全部鍵值對 rehash 到 ht[1] 中,同時將 rehashidx 加 1.

  4. 隨着操做不斷進行,最終在某個時間點上, ht[0] 全部的鍵值對所有 rehash 到 ht[1] 上,這是講 rehashidx 屬性置爲 -1,,表示 rehash操做完成.

在漸進式 rehash 執行期間,新添加到字典的鍵值對一概保存到 ht[1] 裏,不會對 ht[0] 作添加操做,這一措施保證了 ht[0]只減不增,並隨着 rehash 進行, 最終編程空表.

漸進式的 rehash 避免了集中式 rehash 帶來 的龐大計算量和內存操做.

相關文章
相關標籤/搜索