Redis 中,字典是基礎結構。Redis 數據庫數據、過時時間、哈希類型都是把字典做爲底層結構。html
哈希表的實現代碼在:dict.h/dictht
,Redis 的字典用哈希表的方式實現。redis
typedef struct dictht { // 哈希表數組,俗稱的哈希桶(bucket) dictEntry **table; // 哈希表的長度 unsigned long size; // 哈希表的長度掩碼,用來計算索引值,保證不越界。老是 size - 1 // h = dictHashKey(ht, he->key) & n.sizemask; unsigned long sizemask; // 哈希表已經使用的節點數 unsigned long used; } dictht;
table
是一個哈希表數組,每一個節點的實如今 dict.h/dictEntry
,每一個 dictEntry
保存一個鍵值對。size
屬性記錄了向系統申請的哈希表的長度,不必定都用完,有預留空間的。sizemask
屬性主要是用來計算 索引值 = 哈希值 & sizemask
,這個索引值決定了鍵值對放在 table
的哪一個位置。它的值老是 size - 1
,其實我有點不明白爲啥計算的時候不直接用 size - 1
,知道的大佬請明示。used
屬性用來記錄已經使用的節點數,size
- use
就是未使用的節點啦。下圖展現了一個大小爲 4 的空哈希表結構,沒有任何鍵值對
算法
哈希表 dictht
的 table
的元素由哈希節點 dictEntry
組成,每個 dictEntry
就是一個鍵值對數據庫
typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; double d; } v; // 下一個哈希節點,用於哈希衝突時拉鍊表用的 struct dictEntry *next; } dictEntry;
next 指針是用於當哈希衝突的時候,能夠造成鏈表用的。後續會將數組
Redis 的字典實如今: dict.h/dict
。函數
typedef struct dict { // 哈希算法 dictType *type; // 私有數據,用於不一樣類型的哈希算法的參數 void *privdata; // 兩個哈希表,用兩個的緣由是 rehash 擴容縮容用的 dictht ht[2]; // rehash 進行到的索引值,當沒有在 rehash 的時候,爲 -1 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 正在跑的迭代器 unsigned long iterators; /* number of iterators currently running */ } dict; // dictType 實際上就是哈希算法,不知道爲啥名字叫 dictType typedef struct dictType { // hash方法,根據 key 計算哈希值 uint64_t (*hashFunction)(const void *key); // 複製 key void *(*keyDup)(void *privdata, const void *key); // 複製 value void *(*valDup)(void *privdata, const void *obj); // key 比較 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 銷燬 key void (*keyDestructor)(void *privdata, void *key); // 銷燬 value void (*valDestructor)(void *privdata, void *obj); } dictType;
dictType
屬性表示字典類型,實際上這個字典類型就是一組操做鍵值對算法,裏面規定了不少函數。
privdata
則是爲不一樣類型的 dictType
提供的可選參數。
若是有須要,在建立字典的時候,能夠傳入dictType
和 privdata
。性能
dict.cui
// 建立字典,這裏有 type 和 privdata 能夠傳 dict *dictCreate(dictType *type, void *privDataPtr) { dict *d = zmalloc(sizeof(*d)); _dictInit(d,type,privDataPtr); return d; } // 初始化字典 int _dictInit(dict *d, dictType *type, void *privDataPtr) { _dictReset(&d->ht[0]); _dictReset(&d->ht[1]); d->type = type; d->privdata = privDataPtr; d->rehashidx = -1; d->iterators = 0; return DICT_OK; }
下圖是比較完整的普通狀態下的 dict
的結構(沒有進行 rehash,也沒有迭代器的狀態):
# 哈希算法
當字典中須要添加新的鍵值對時,須要先對鍵進行哈希,算出哈希值,而後在根據字典的長度,算出索引值。spa
// 使用哈希字典裏面的哈希算法,算出哈希值 hash = dict->type->hashFunction(key) // 使用 sizemask 和 哈希值算出索引值 idx = hash & d->ht[table].sizemask; // 經過索引值,定位哈希節點 he = d->ht[table].table[idx];
哈希衝突指的是多個不一樣的 key,算出的索引值同樣。設計
Redis 解決哈希衝突的方法是:拉鍊法。就是每一個哈希節點後面有個 next
指針,當發現計算出的索引值對應的位置有其餘節點,那麼直接加在前面節點後便可,這樣就造成了一個鏈表。
下圖展現了 {k1, v1}
和 {k2, v2}
哈希衝突的結構。
假設 k1
和 k2
算出的索引值都是 3,當 k2
發現 table[3]
已經有 dictEntry{k1,v1}
,那就 dictEntry{k1,v1}.next = dictEntry{k2,v2}
。
隨着操做的不斷進行,哈希表的長度會不斷增減。哈希表的長度太長會形成空間浪費,過短哈希衝突明顯致使性能降低,哈希表須要經過擴容或縮容,讓哈希表的長度保持在一個合理的範圍內。
Redis 經過 ht[0] 和 ht[1] 來完成 rehash 的操做,步驟以下:
ht[0].used * 2
的 \(2^n\) 的數,例如 ht[0].used = 3,那麼分配的是距離 6 最近的 \(2^3=8\)ht[0].used / 2
的 \(2^n\) 的數,例如 ht[0].used = 6,那麼分配的是距離 3 最近的 \(2^2=4\)h[0] = h[1]
,並把 h[1] 清空,爲下次 rehash 準備上面說的 rehash 中的第二步,遷移的過程不是一次完成的。若是哈希表的長度比較小,一次完成很快。可是若是哈希表很長,例如百萬千萬,那這個遷移的過程就沒有那麼快了,會形成命令阻塞!
下面來講說,redis 是如何漸進式地將 h[0]
中的鍵值對遷移到 h[1]
中的:
rehashidx
維護了 rehash 的進度,設置爲 0 的時候,開始 rehashrehashidx
上的整條鏈表遷移到 h[1]
中。遷移完以後 rehashidx + 1
h[0]
上的全部鍵值對都會遷移到 h[1]
中。所有遷移完成以後 rehashidx = -1
這種漸進式 rehash 的方式的好處在於,將龐大的遷移工做,分攤到每次的增刪改查中,避免了一次性操做帶來的性能的巨大損耗。
缺點就是遷移過程當中 h[0]
和 h[1]
同時存在的時間比較長,空間利用率較低。
下面一系列的圖,演示了字典是如何漸進式地 rehash ( 圖片來自 《Redis 設計與實現》圖片集 )