Redis 字典結構細談

Redis 字典底層基於哈希表實現。

一、哈希表結構

1、dictht

typedef struct dictht {

  dictEntry **table; //哈希表數組,存儲具體的鍵值對元素,對象類型 dictEntry

  unsigned long size; //哈希表容量

  unsigned long sizemask; //哈希表大小掩碼,計算索引使用

  unsigned long used; //已使用容量

} dictht

2、示例數據:

 

二、哈希表節點

1、dictEntry

typedef struct dictEntry {

  void *key; //鍵值對 key

  union{  //鍵值對 value 三種類型

    void *val;

    uint64_tu64;

    int64_ts64;

  } v;

  struct dictEntry *next;  //下一個節點指針

} dictEntry;

說明:next 為指向下一個節點的指針,是我們熟悉的鏈表節點結構,單向鏈表,用於處理鍵哈希衝突問題。

相同哈希值的鍵的鍵值對會以鏈表的形式存在同一位置。

2、示例數據:

 

三、Redis 字典

1、dict

typedef struct dict{

  dictType *type; //類型特定函數

  void *privdata; //私有數據

  dictht ht[2]; //哈希表數組,類型為dicththt[0]為實際存儲數據使用,ht[1] rehash時使用

  int rehashidx; //rehash進度標誌,-1 代表當前不在 rehash

} dict

2、示例數據:

 

四、添加元素

向字典中添加元素主要涉及一下幾步操作:

1、計算鍵值對鍵的哈希值

hashdict->type->hashFunction(key)

使用dictType內部的哈希函數得到鍵哈希值

2、計算需要放入的位置索引

indexhash&dict->ht[0].sizemask

使用上一步計算得到的哈希值與哈希表的sizemask屬性進行與操作得到需要放入的位置索引值

3、鍵衝突解決

沒有完美的哈希函數,哈希衝突往往無法避免,當多個鍵被所引導同一個位置時,這種現象,我們稱之為鍵衝突。

解決間衝突,Redis 採用鏈地址法,也即將衝突的鍵值對組成一條鏈條放到同一個哈希位置上。上面第二節我們介紹過 dictEntry的結構,其中包含一個指向另一個節點的指針next

這裡需要說明的一點是,衝突節點插入時,是插入到鏈表的頭部,這樣只需要執行操作一次操作即可,也即時間複雜度為O(1)

如下圖:(k2v2)與(k1v1)發生衝突,直接將(k2v2插入到鏈表頭部:

五、rehash

rehash過程是在重新規劃哈希表佔用空間時發生的。

負載因子 load_factor:已保存節點數量(dict.ht[0].used/ 哈希表容量dict.ht[0].size

負載因子用以表名當前哈希表的使用狀態,它需要保持在一個合理的範圍,以保障資源的最優利用。通常需要適時的對哈希表進行擴展或者收縮來對負載因子進行維護,而這個過程,我們稱之為 rehash

這裡涉及到一個問題,就是什麼時候需要進行伸縮維護?

1、擴展時機:

當前無bgsavebgrewriteaop操作,load_factor >= 1

當前存在bgsavebgrewriteaop操作,load_factor >= 5

Redis服務器通過fork子進程形式執行bgsavebgrewriteaop操作,此時整個服務的資源耗費較大,為了避免可能發生的rehash帶來額外的資源壓力,此期間,服務器會調高觸發執行擴展操作的負載因子界限。

2、收縮時機:

load_factor < 0.1

3、rehash 基本操作:

a) 為dict.ht[1]分配空間:

空間大小計算如下:

擴展:最小n滿足2n >= dict.ht[0].used * 2

收縮:最小n滿足2n >= dict.ht[0].used

如下圖:ht[0].used = 3,假定無bg相關任務,則h[1]大小需要計算:2n >= 3 * 2 = 6

n = 3ht[1].size = 23 = 8

 

b) rehash

對於dict.ht[0] 中的元素,依據dict.ht[1]特性(sizemask)重新計算索引值,並放置到dict.ht[1]中。

c) 當所有元素遷移完畢,釋放dict.ht[0],並將dict.ht[1]設置為dict.ht[0],重新在dict.ht[1]上創建空的哈希表。

六、漸進式rehash

所謂漸進式,是針對大數據量字典數據。直接一次性的執行rehash會導致服務資源的集中佔用,影響正常的服務響應。因此需要進行分而治之。

這裡會用到上面我們介紹的dict字典結構中的 rehashidx屬性,用以標識當前rehash進度。

首先將rehashidx0,標示rehash開始,每次rehash一個元素,rehashidx值增加1,當最終所有元素rehash完成,將rehashidx-1

這裡需要說明下rehash中對正常的服務請求的處理:

1、刪除、查找、更新:

會涉及到兩個哈希表(ht[0]ht[1])操作,如查找元素,首先嘗試在ht[0]上查找,找不到,則繼續在h[1]上查找。

2、添加

添加元素只會在h[1]上操作,h[0]上只減不增。