Redis 字典實現

4.1 字典數據結構

typedef struct dict{
  //類型特定函數
  dictType *type;
  //私有數據
  void *privateata;
  //哈希表
  dictht ht[2];
  //rehash 索引,rehash未進行時,值爲-1
  int rehashidx;
}dict;
  • 其中的type 是一個指向 dictType 結構的指針,每一個 dictType 結構保存了一簇用於操做特定類型鍵值對的函數,Redis 會爲用途不一樣的字典設置不一樣的類型特定函數。
typedef struct dictType{
  //計算哈希值的函數
  unsigned int (*hashFunction)(const void *key);
  //複製鍵的函
  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;
  • privdata屬性則保存了須要傳給那些類型特定函數的可選參數。
  • ht是一個包含兩個項的數組,數組中的每項都是dictht哈希表,通常狀況下,字典只是用 ht[0]哈希表,ht{1]只有在對ht[0]哈希表進行rehash時使用
// Redis 字典使用的哈希表由 dict 標識,Redis的字典使用哈希表做爲底層實現,
//一個哈希表裏面能夠有多個哈希表節點,而每一個哈希表節點就保存了字典中的一個鍵值對。
typedef struct dictht {
  //哈希表數組
  dictEntry **table;
  //哈希表大小
  unsigned long size;
  //哈希表大小掩碼,用於計算索引值,老是等於size-1
  unsigned long sizemask;
  //該哈希表已有節點的數量
  unsigned long used;
}dictht;
//哈希表節點用dictEntry標識,每一個dictEntry保存了一個鍵值對
typedef struct dictEntry {
  //鍵
  void *key;
  //值
  union{    
    void *val;         
    uint64_tu64;    
    int64_ts64; 
  } v;
  //指向下個哈希表節點,造成鏈表,解決地址衝突問題  
  struct dictEntry *next;
} dictEntry;
  • rehashidx記錄了rehash目前的進度,若是目前沒有在進行rehash,那麼它的值爲-1。算法

 

 4.2 哈希算法

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

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

#使用字典設置的哈希函數,計算哈希值服務器

hash = dict -》 type -》 hashFunction(key)數據結構

#使用哈希表的sizemask 屬性和哈希值hash,計算出索引值函數

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

index = hash & dictht -> ht[x].sizemaskui

當字典被用做數據庫的底層實現或哈希鍵的底層實現時,Redis 使用的是 Murmurhash2 算法來計算hash 值。spa

4.3 解決鍵衝突

Redis 使用鏈地址法來解決鍵衝突。每一個哈希表節點有一個next指針,多個哈希表節點經過next 指針構成一個單向鏈表。由於 dictEntry 節點組成的鏈表沒有指向鏈表表尾的指針,因此爲了速度考慮,老是將新節點添加到鏈表的表頭位置(複雜度爲O(1)),排在其餘已有節點前面。(k2,v2)是新添加的節點。3d

 4.4 rehash

隨着操做的不斷執行,哈希表保存的鍵值對會逐漸的增多或減小,爲了讓哈希表的負載因子維持在一個合理的閾值以內,當哈希表的鍵值對的數量太多或太少時,對哈希表進行相應的擴展或收縮。

擴展和收縮哈希表經過rehash(從新散列)進行,具體步驟以下

  1. 爲字典ht[1]分配空間,這個哈希表空間大小取決於要執行的操做,以及ht[0]當前包含的鍵值對的數量(也就是ht[0].used屬性)
    1. 擴展操做:ht[1]的大小爲第一個大於等於ht[0].used * 2 的 2^n(2的n次冪)
    2. 收縮操做:ht[1]的大小爲第一個大於等於ht[0].used 的 2^n
  2. 將保存在ht[0]中的全部鍵值對 rehash 到 ht[1]上面:rehash 是指從新計算鍵的哈希值和索引值,而後將鍵值對放到 ht[1]哈希表的指定位置
  3. 當ht[0]所有遷移到ht[1]後,釋放ht[0],將ht[1] 設置爲 ht[0],並在 ht[1]上新建一個空白哈希表,爲下一次 rehash 作準備。

4.5 漸進式rehash

爲了不鍵值對過多的 rehash(涉及到龐大的計算量) 對服務器性能形成影響,服務器不是一次將ht[0] 上的全部鍵值對 rehash 到 ht[1],而是分屢次、漸進式的將 ht[0] 裏全部的鍵值對進行遷移。

漸進式hash 的步驟:

  1. 爲ht[1]分配空間,讓字典同時持有 ht[0] ht[1]
  2. 在字典中維持一個索引計數器變量 rehashidx,並將其設置爲0,標識 rehash 開始
  3. 在 rehash 期間,每次對字典的添加、刪除、查找或更新等,程序除了執行指定的操做外,還會將ht[0] 哈希表在 rehashidx 索引上的全部鍵值對 rehash 到 ht[1] 上,當rehash 完成後,程序將 rehashidx 值加一
  4. 最終,ht[0]所有 rehash 到 ht[1] 上,這時程序將 rehashidx 值設置爲 -1,標識 rehash 完成

漸進式rehash 將rehash 的工做均攤到每一個添加、刪除、查找和更新中,從而避免集中rehash帶來的問題。

相關文章
相關標籤/搜索