Redis哈希表總結

本文及後續文章,Redis版本均是v3.2.8

在文章《Redis 數據結構之dict》《Redis 數據結構之dict(2)》中,從代碼層面作了簡單理解。總感受思路的不夠條理性,特開一篇文章把哈希表中幾個知識點串聯下。算法

1、先來回顧下哈希表結構定義

/**數組

 * 哈希表bash

 */服務器

typedef struct dictht {數據結構

    // 哈希表節點指針數組(俗稱桶,bucket)函數

    dictEntry **table;性能

    // 指針數組的大小ui

    unsigned long size;設計

    // 指針數組的長度掩碼,用於計算索引值3d

    unsigned long sizemask;

    // 哈希表現有的節點數量

    unsigned long used;

} dictht;

 

table屬性是一個數組,數組中的每一個元素都是一個指向dict.h/dictEntry結構的指針,每一個dictEntry結構保存着一個鍵值對。size屬性記錄了哈希表的大小,也便是table數組的大小,而used屬性則記錄了哈希表目前已有節點(鍵值對)的數量。sizemask屬性的值老是等於size-1,這個屬性和哈希值一塊兒決定一個鍵應該被放到table數組的哪一個索引上面。

一個大小爲4的空哈希表結構:

圖1-1 大小爲4的空哈希表

哈希表節點結構定義

/**

 * 哈希表節點

 */

typedef struct dictEntry {

    // 鍵

    void *key;

    // 值

    union {

        void *val;

        uint64_t u64;

        int64_t s64;

    } v;

    // 鏈日後繼節點

    struct dictEntry *next;

} dictEntry;

 

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

key屬性保存着鍵值對中的鍵,而v屬性則保存着鍵值對中的值,其中鍵值對的值能夠是一個指針,或者是一個uint64t整數,又或者是一個int64t整數。

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

 

2、哈希算法

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

 

Redis計算哈希值和索引值的方法以下:

  • 使用字典設置的哈希函數.計算鍵key的哈希值

       hash=dict->type->hashFunction(key);

  • 使用哈希表的sizemask屬性和哈希值,計算出索引值

  • 根據狀況不一樣,ht[x]能夠是ht[0]或者ht[1]

       index=  hash&dict->ht[x].sizemask 

 

舉個例子,對於空的字典來講,若是咱們要將一個鍵值對k0和v0添加到字典裏面。

那麼程序會先使用語句:

hash=dict->type->hashFunction(k0);

計算鍵k0的哈希值。

假設計算得出的哈希值爲8,那麼程序會繼續使用語句:

index = hash&dict->ht[0] .sizemask = 8 & 3 = 0;

計算出鍵k0的索引值0,這表示包含鍵值對k0和v0的節點應該被放置到哈希表數組的索引0位置上。

空的字典、添加鍵值對k0和v0的結構,以下圖所示。

圖1-2空字典

圖1-3 添加鍵值對K0和V0以後的字典

 

3、解決鍵衝突

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

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

舉個例子,假設程序要將鍵值對k2和v2添加到圖1-4所示的哈希表裏面,而且計算得出k2的索引值爲2,那麼鍵k1和k2將產生衝突,而解決衝突的辦法就是使用next指針將鍵k2和k1所在的節點鏈接起來,如圖1-5所示。

由於dictEntry節點組成的鏈表沒有指向鏈表表尾的指針,因此爲了速度考慮,程序老是將新節點添加到鏈表的表頭位置(複雜度爲0(1)),排在其餘已有節點的前面。

圖1-4 一個包含兩個鍵值對的哈希表

圖1-5 使用鏈表解決k1和k2的衝突

 

4、Rehash

上兩篇文章提到dict結構中ht屬性是一個包含兩個項的數組,數組中的每一個項都是一個dictht晗希表,通常狀況下,字典只使用ht[0)哈希表,ht[1]哈希表只會在對ht[0]哈希表進行rehash時使用。除了ht[1]以外,另外一個和rehash有關的屬性就是rehashidx,它記錄了rehash目前的進度,若是目前沒有在進行rehash,那麼它的值爲-1。

咱們看下一個普通狀態下的字典即沒有進行rehash的字典:

圖1-6 沒有進行rehash的字典

隨着操做的不斷執行,哈希表中保存的鍵值對會逐漸地增多或者減小,爲了讓哈希表的負載因子(loadfactor)維持在一個合理的範圍以內,當哈希表保存的鍵值對數量太多或者太少時,程序須要對哈希表的大小進行相應的擴展或者收縮 。若是節點數量比哈希表的大小要大不少的話,那麼哈希表就會退化成多個鏈表,哈希表自己的性能優點便不復存在。這個就是咱們上篇文章中說到的哈希表擴展和收縮策略。

擴展和收縮哈希表的工做能夠經過執行   rehash  (從新散列)操做來完成, Redis對字典的哈希表執行rehash的步驟以下:

  • 爲字典的ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操

    做,以 及ht[0]當前包含的鍵值對數量(也便是ht[0].used屬性的值):

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

的2"(2的n次方幕)。

        二、若是執行的是收縮操做,那麼ht[1]的大小爲第一個大於等於ht[0].used的2"(2的n次方幕)。 

  • 將保存在ht[0]中的全部鍵值對rehash到ht[1]上面:  rehash指的是從新計

算鍵的哈希值和索引值,而後將鍵值對放置到ht[1]晗希表的指定位置上。

  • 當ht[0]包含的全部鍵值對都遷移到了ht[1)以後(ht[0]變爲空表),釋放ht[0],將ht[1]設置爲ht[0],並在ht[1]新建立一個空白哈希表,爲下一次rehash作準備。

舉個例子,咱們對下圖1-7所示字典的ht[0]進行擴展操做,那麼程序執行的過程是怎麼樣一個過程哪?

圖1-7 執行rehash以前的字典

一、ht[0].used當前的值爲4,4*2=8,而8(23)剛好是第一個大於等於4的2的n次方,因此程序會將ht[l]晗希表的大小設置爲8。圖1-8展現了ht[1]在分配空間以後,字典的樣子。

圖1-8

2)將ht[0]包含的四個鍵值對都rehash到ht[1],如圖1-9所示 

圖1-9

3)釋放ht[0],並將ht[l]設置爲ht[0],而後爲ht[l]分配一個空白哈希表,如

圖1-10所示。至此,對哈希表的擴展操做執行完畢,程序成功將哈希表的大小從原來的 4改成了如今的8。

圖1-10

4、漸進式rehash

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

這樣作的緣由在於,若是  ht[0]裏只保存着四個鍵值對,那麼服務器能夠在瞬間就將這些鍵值對所有rehash到ht[1]。可是,若是哈希表裏保存的鍵值對數量幾千甚至上萬百萬個鍵值對,那麼要一次性將這些鍵值對所有rehash到ht[1]的話,龐大的計算量可能會致使服務器在一段時間內中止服務。

所以,爲了不rehash對服務器性能形成影響,服務器不是一次性將ht[0]裏面的全部鍵值對所有rehash到ht[1],而是分屢次、漸進式地將ht[0]裏面的鍵值對慢慢地rehash到ht[1]。

 

如下是哈希表漸進式rehash的詳細步驟:

一、爲ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。

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

三、在rehash進行期間,每次對字典執行添加、刪除、查找或者更新操做時,程序除了執行指定的操做之外,還會順帶將ht[0]哈希表在rehashidx索引上的全部鍵值對rehash到ht[1]。當rehash工做完成以後,程序將rehashidx屬性的值增長1。

四、隨着字典操做的不斷執行,最終在某個時間點上,ht[0]的全部鍵值對都會被rehash至ht[1],這時程序將rehashidx屬性的值設爲-1,表示rehash操做已完成。

漸進式rehash的好處在於它採起分而治之的方式,將 rehash鍵值對所需的計算工做均攤到對字典的每一個添加、刪除、查找和更新操做上,從而避免了集中式  rehash而帶來的龐大計算量。

 

由於在進行漸進式rehash的過程當中,字典會同時使用ht[0]和ht[l]兩個哈希表,因此在漸進式rehash進行期間,字典的刪除(delete)、查找(find)、更新(update)等操做會在兩個哈希表上進行。例如,要在字典裏面查找一個鍵的話,程序會先在ht[0]裏面進行查找,若是沒找到的話,就會繼續到ht[l]裏面進行查找,諸如此類。

另外,在漸進式rehash執行期間,新添加到字典的鍵值對一概會被保存到ht[1]裏面,而ht[0]則再也不進行任何添加操做,這一措施保證了ht[0]包含的鍵值對數量會只減不增,並隨着rehash操做的執行而最終變成空表。

 

圖1-11至圖1-16展現了一次完整的漸進式rehash過程,注意觀察在整個rebash過程當中,字典的 rehashidx屬性是如何變化的?

圖1-11 準備開始rehash

圖1-12 rehash索引0上的鍵值對

圖1-13 rehash索引1上的鍵值對

圖1-14 rehash索引2上的鍵值對

圖1-15 rehash索引3上的鍵值對

圖1-16 rehash執行完畢

 

參考:《Redis設計與實踐》

-EOF-

相關文章
相關標籤/搜索