字典,是一種用於保存鍵值對的抽象數據結構。因爲 C 語言沒有內置字典這種數據結構,所以 Redis 構建了本身的字典實現。數據庫
在 Redis 中,就是使用字典來實現數據庫底層的。對數據庫的 CURD 操做也是構建在對字典的操做之上。數組
除了用來表示數據庫以外,字典仍是哈希鍵的底層實現之一。當一個哈希鍵包含的鍵值對比較多,又或者鍵值對中的元素都是比較長的字符串時,Redis 就會適應字典做爲哈希鍵的底層實現。服務器
Redis 的字典使用哈希表做爲底層實現。一個哈希表裏面能夠有多個哈希表節點,而每一個哈希表節點就保存了字典中的一個鍵值對。數據結構
Redis 字典所使用的哈希表結構:函數
typedef struct dictht { dictEntry **table; // 哈希表數組 unsigned long size; // 哈希表大小 unsigned long sizemask; // 哈希表大小掩碼,用來計算索引 unsigned long used; // 哈希表現有節點的數量 } dictht;
圖 1 展現了一個大小爲 4 的空哈希表。性能
哈希表節點使用 dictEntry 結構表示,每一個 dictEntry 結構中都保存着一個鍵值對:ui
typedef struct dictEntry { void *key; // 鍵 union { void *val; // 值類型之指針 uint64_t u64; // 值類型之無符號整型 int64_t s64; // 值類型之有符號整型 double d; // 值類型之浮點型 } v; // 值 struct dictEntry *next; // 指向下個哈希表節點,造成鏈表 } dictEntry;
圖 2 展現了經過 next 指針,將兩個索引相同的鍵 k1 和 k0 鏈接在一塊兒的狀況。3d
字典的結構:指針
typedef struct dict { dictType *type; // 類型特定函數 void *privdata; // 私有數據 dictht ht[2]; // 哈希表(兩個) long rehashidx; // 記錄 rehash 進度的標誌。值爲 -1 表示 rehash 未進行 int iterators; // 當前正在迭代的迭代器數 } dict;
dictType 的結構以下:
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;
type 屬性和 privdata 屬性是針對不一樣類型的鍵值對,爲建立多態字典而設置的。其中:
而 ht 屬性是一個包含兩個哈希表的數組。通常狀況下,字典只使用 ht[0],只有在對 ht[0] 進行 rehash 時纔會使用 ht[1]。
rehashidx 屬性,它記錄了 rehash 目前的進度,若是當前沒有進行 rehash,它的值爲 -1。至於什麼是 rehash,別急,後面會詳細說明。
圖 3 是沒有進行 rehash 的字典:
當在字典中添加一個新的鍵值對時,Redis 會先根據鍵值對的鍵計算出哈希值和索引值,而後再根據索引值,將包含新鍵值對的哈希表節點放到哈希表數組指定的索引上。具體算法以下:
# 使用字典設置的哈希函數,計算 key 的哈希值 hash = dict->type->hashFunction(key); # 使用哈希表的 sizemask 屬性和哈希值,計算出索引值 # 根據不一樣狀況,使用 ht[0] 或 ht[1] index = hash & dict[x].sizemask;
如圖 4,若是把鍵值對 [k0, v0] 添加到字典中,插入順序以下:
hash = dict-type->hashFunction(k0); index = hash & dict->ht[0].sizemask; # 8 & 3 = 0
計算得出,[k0, v0] 鍵值對應該被放在哈希表數組索引爲 0 的位置上,如圖 5:
當有兩個或以上數量的鍵被分配到了哈希表數組的同一個索引上面時,咱們認爲這些鍵發生了建衝突。
Redis 的哈希表使用鏈地址法來解決建衝突。每一個哈希表節點都有一個 next 指針,多個哈希表節點能夠用 next 指針構成一個單向鏈表,被分配到同一個索引的多個節點用 next 指針連接成一個單向鏈表。
舉個栗子,假設咱們要把 [k2, v2] 鍵值對添加到圖 6 所示的哈希表中,而且計算得出 k2 的索引值爲 2,和 k1 衝突,所以,這裏就用 next 指針將 k2 和 k1 所在的節點鏈接起來,如圖 7。
隨着對字典的操做,哈希表報錯的鍵值對會逐漸增多或者減小,爲了讓哈希表的負載因子維持在一個合理的範圍以內,當哈希表報錯的鍵值對數量太多或者太少時,程序須要對哈希表進行相應的擴容或收縮。這個擴容或收縮的過程,咱們稱之爲 rehash。
對於負載因子,能夠經過如下公式計算得出:
# 負載因子 = 哈希表已保存節點數量 / 哈希表大小 load_factor = ht[0].used / ht[0].size;
擴容
對於哈希表的擴容,源碼以下:
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); }
當如下條件被知足時,程序會自動開始對哈希表執行擴展操做:
收縮
哈希表的收縮,源碼以下:
int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); // ht[2] 兩個哈希表的大小之和 used = dictSize(dict); // ht[2] 兩個哈希表已保存節點數量之和 # DICT_HT_INITIAL_SIZE 默認爲 4,HASHTABLE_MIN_FILL 默認爲 10。 return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL)); } void tryResizeHashTables(int dbid) { if (htNeedsResize(server.db[dbid].dict)) dictResize(server.db[dbid].dict); if (htNeedsResize(server.db[dbid].expires)) dictResize(server.db[dbid].expires); }
當 ht[] 哈希表的大小之和大於 DICT_HT_INITIAL_SIZE(默認 4),且已保存節點數量與總大小之比小於 4,HASHTABLE_MIN_FILL(默認 10,也就是 10%),會對哈希表進行收縮操做。
擴容和收縮哈希表都是經過執行 rehash 操做來完成,哈希表執行 rehash 的步驟以下:
示例:
假設程序要對圖 8 所示字典的 ht[0] 進行擴展操做,那麼程序將執行如下步驟:
1)ht[0].used 當前的值爲 4,那麼 4*2 = 8,而 2^3 剛好是第一個大於等於 8 的,2 的 n 次方。因此程序會將 ht[1] 哈希表的大小設置爲 8。圖 9 是 ht[1] 在分配空間以後的字典。
2)將 ht[0] 包含的四個鍵值對都 rehash 到 ht[1],如圖 10。
3)釋放 ht[0],並將 ht[1] 設置爲 ht[0],而後爲 ht[1] 分配一個空白哈希表。如圖 11:
至此,對哈希表的擴容操做執行完畢,程序成功將哈希表的大小從原來的 4 改成了 8。
對於 Redis 的 rehash 而言,並非一次性、集中式的完成,而是分屢次、漸進式地完成,因此也叫漸進式 rehash。
之因此採用漸進式的方式,其實也很好理解。當哈希表裏保存了大量的鍵值對,要一次性的將全部鍵值對所有 rehash 到 ht[1] 裏,極可能會致使服務器在一段時間內只能進行 rehash,不能對外提供服務。
所以,爲了不 rehash 對服務器性能形成影響,Redis 分屢次、漸進式的將 ht[0] 裏面的鍵值對 rehash 到 ht[1]。
漸進式 rehash 就用到了索引計數器變量 rehashidx,詳細步驟以下:
漸進式 rehash 纔有分而治之的方式,將 rehash 鍵值對所須要的計算工做均攤到對字典的 CURD 操做上,從而避免了集中式 rehash 帶來的問題。
此外,字典在進行 rehash 時,刪除、查找、更新等操做會在兩個哈希表上進行。例如,在字典張查找一個鍵,程序會如今 ht[0] 裏面進行查找,若是沒找到,再去 ht[1] 上查找。
要注意的是,新增的鍵值對一概只保存在 ht[1] 裏,不在對 ht[0] 進行任何添加操做,保證了 ht[0] 包含的鍵值對數量只減不增,隨着 rehash 操做最終變成空表。
圖 12 至 圖 17 展現了一次完整的漸進式 rehash 過程:
1)未進行 rehash 的字典
2) rehash 索引 0 上的鍵值對
3)rehash 索引 1 上的鍵值對
4)rehash 索引 2 上的鍵值對
5)rehash 索引 3 上的鍵值對
6)rehash 執行完畢