Redis5.0原始碼解析(三)----------字典(詳細)
基於Redis5.0
在字典中, 一個鍵(key)可以和一個值(value)進行關聯(或者說將鍵對映為值), 這些關聯的鍵和值就被稱為鍵值對
字典中的每個鍵都是獨一無二的, 程式可以在字典中根據鍵查詢與之關聯的值, 或者通過鍵來更新值, 又或者根據鍵來刪除整個鍵值對, 等等
字典的實現
Redis 的字典使用雜湊表作為底層實現, 一個雜湊表裡面可以有多個雜湊表節點, 而每個雜湊表節點就儲存了字典中的一個鍵值對
雜湊表
Redis 字典所使用的雜湊表由 dict.h/dictht 結構定義:
//dict.h Hash Tables Implementation.
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
// 雜湊表陣列
dictEntry **table;
// 雜湊表大小
unsigned long size;
// 雜湊表大小掩碼,用於計算索引值
// 總是等於 size - 1
unsigned long sizemask;
// 該雜湊表已有節點的數量
unsigned long used;
} dictht;
-
table
屬性是一個數組, 陣列中的每個元素都是一個指向 dict.h/dictEntry 結構的指標, 每個 dictEntry 結構儲存著一個鍵值對 -
size
屬性記錄了雜湊表的大小, 也即是 table 陣列的大小 -
sizemask
屬性的值總是等於 size - 1 , 這個屬性和雜湊值一起決定一個鍵應該被放到 table 陣列的哪個索引上面。 -
used
屬性則記錄了雜湊表目前已有節點(鍵值對)的數量
雜湊表節點
雜湊表節點使用 dictEntry 結構表示, 每個 dictEntry 結構都儲存著一個鍵值對:
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下個雜湊表節點,形成連結串列
struct dictEntry *next;
} dictEntry;
key
屬性儲存著鍵值對中的鍵v
屬性則儲存著鍵值對中的值, 其中鍵值對的值可以是一個指標, 或者是一個 uint64_t 整數, 又或者是一個 int64_t 整數,或者是double型別的浮點數next
屬性是指向另一個雜湊表節點的指標, 這個指標可以將多個雜湊值相同的鍵值對連線在一次, 以此來解決鍵衝突(collision)的問題
字典
Redis 中的字典由 dict.h/dict 結構表示:
typedef struct dict {
// 型別特定函式
dictType *type;
// 私有資料
void *privdata;
// 雜湊表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值為 -1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
//字典所持有的迭代器,只有是安全迭代器時才會增加此欄位
unsigned long iterators; /* number of iterators currently running */
} dict;
type
屬性和 privdata
屬性是針對不同型別的鍵值對, 為建立多型字典而設定的:
type
屬性是一個指向dictType
結構的指標, 每個dictType
結構儲存了一簇用於操作特定型別鍵值對的函式, Redis 會為用途不同的字典設定不同的型別特定函式。privdata
屬性則儲存了需要傳給那些型別特定函式的可選引數
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;
-
ht
屬性是一個包含兩個項的陣列, 陣列中的每個項都是一個dictht
雜湊表, 一般情況下, 字典只使用ht[0]
雜湊表,ht[1]
雜湊表只會在對ht[0]
雜湊表進行 rehash 時使用 -
rehashidx
: 它記錄了 rehash 目前的進度, 如果目前沒有在進行 rehash , 那麼它的值為 -1 -
iterators
:字典迭代器的數量,只有是安全迭代器時(dictIterator->safe = 1
)才會增加此欄位
迭代器
字典的迭代器由 dict.h/dictIterator 結構表示:
/* If safe is set to 1 this is a safe iterator, that means, you can call
* dictAdd, dictFind, and other functions against the dictionary even while
* iterating. Otherwise it is a non safe iterator, and only dictNext()
* should be called while iterating. */
typedef struct dictIterator {
//指向迭代器處理的字典
dict *d;
//dictht中table陣列的下標
long index;
//table是dict結構中dictht陣列的下標,即標識ht[0]還是ht[1]。
//safe欄位用於標識該迭代器是否為一個安全的迭代器
int table, safe;
//entry和nextEntry分別指向當前的元素和下一個元素
dictEntry *entry, *nextEntry;
//fingerprint是字典的指紋
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint;
} dictIterator;
safe
欄位用於標識該迭代器是否為一個安全的迭代器。如果是,則可以在迭代過程中使用dictDelete
、dictFind
等方法;如果不是,則只能使用dictNext
遍歷方法
迭代器提供了遍歷字典中所有元素的方法,通過dicGetIterator()
獲得迭代器後,使用dictNext(dictIterator *)
獲得下一個元素。遍歷的過程,先從ht[0]
開始,依次從第一個桶table[0]
開始遍歷桶中的元素,然後遍歷table[1]
,'***
,table[size]
,若正在擴容,則會繼續遍歷ht[1]
中的桶。遍歷桶中元素時,依次訪問連結串列中的每一個元素。
更方便的巨集定義:
/* ------------------------------- Macros ------------------------------------*/
#define dictFreeVal(d, entry) \
if ((d)->type->valDestructor) \
(d)->type->valDestructor((d)->privdata, (entry)->v.val)
#define dictSetVal(d, entry, _val_) do { \
if ((d)->type->valDup) \
(entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
else \
(entry)->v.val = (_val_); \
} while(0)
#define dictSetSignedIntegerVal(entry, _val_) \
do { (entry)->v.s64 = _val_; } while(0)
#define dictSetUnsignedIntegerVal(entry, _val_) \
do { (entry)->v.u64 = _val_; } while(0)
#define dictSetDoubleVal(entry, _val_) \
do { (entry)->v.d = _val_; } while(0)
#define dictFreeKey(d, entry) \
if ((d)->type->keyDestructor) \
(d)->type->keyDestructor((d)->privdata, (entry)->key)
#define dictSetKey(d, entry, _key_) do { \
if ((d)->type->keyDup) \
(entry)->key = (d)->type->keyDup((d)->privdata, _key_); \
else \
(entry)->key = (_key_); \
} while(0)
#define dictCompareKeys(d, key1, key2) \
(((d)->type->keyCompare) ? \
(d)->type->keyCompare((d)->privdata, key1, key2) : \
(key1) == (key2))
#define dictHashKey(d, key) (d)->type->hashFunction(key)
#define dictGetKey(he) ((he)->key)
#define dictGetVal(he) ((he)->v.val)
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
#define dictGetDoubleVal(he) ((he)->v.d)
#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
#define dictIsRehashing(d) ((d)->rehashidx != -1)
雜湊演算法
當要將一個新的鍵值對新增到字典裡面時, 程式需要先根據鍵值對的鍵計算出雜湊值和索引值, 然後再根據索引值, 將包含新鍵值對的雜湊表節點放到雜湊表陣列的指定索引上面
Redis 計算雜湊值和索引值的方法如下:
// 使用字典設定的雜湊函式,計算鍵 key 的雜湊值
hash = dict->type->hashFunction(key);
// 使用雜湊表的 sizemask 屬性和雜湊值,計算出索引值
// 根據情況不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
舉個例子, 對於圖 4-4 所示的字典來說, 如果我們要將一個鍵值對 k0 和 v0 新增到字典裡面, 那麼程式會先使用語句:
hash = dict->type->hashFunction(k0);
計算鍵 k0 的雜湊值。
假設計算得出的雜湊值為 8 , 那麼程式會繼續使用語句:
index = hash & dict->ht[0].sizemask = 8 & 3 = 0;
計算出鍵 k0 的索引值 0 , 這表示包含鍵值對 k0 和 v0 的節點應該被放置到雜湊表陣列的索引 0 位置上, 如圖 4-5 所示。
當字典被用作資料庫的底層實現, 或者雜湊鍵的底層實現時, Redis 使用 SipHash演算法來計算鍵的雜湊值
/* -------------------------- hash functions -------------------------------- */
static uint8_t dict_hash_function_seed[16];
void dictSetHashFunctionSeed(uint8_t *seed) {
memcpy(dict_hash_function_seed,seed,sizeof(dict_hash_function_seed));
}
uint8_t *dictGetHashFunctionSeed(void) {
return dict_hash_function_seed;
}
/* The default hashing function uses SipHash implementation
* in siphash.c. */
uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);
uint64_t dictGenHashFunction(const void *key, int len) {
return siphash(key,len,dict_hash_function_seed);
}
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len) {
return siphash_nocase(buf,len,dict_hash_function_seed);
}
解決鍵衝突
當有兩個或以上數量的鍵被分配到了雜湊表陣列的同一個索引上面時, 我們稱這些鍵發生了衝突(collision)
Redis 的雜湊表使用鏈地址法(separate chaining)來解決鍵衝突: 每個雜湊表節點都有一個 next 指標, 多個雜湊表節點可以用 next 指標構成一個單向連結串列, 被分配到同一個索引上的多個節點可以用這個單向連結串列連線起來, 這就解決了鍵衝突的問題
因為 dictEntry
節點組成的連結串列沒有指向連結串列表尾的指標, 所以為了速度考慮, 程式總是將新節點新增到連結串列的表頭位置(複雜度為 O(1)), 排在其他已有節點的前面
Rehash
隨著操作的不斷執行, 雜湊表儲存的鍵值對會逐漸地增多或者減少, 為了讓雜湊表的負載因子(load factor)維持在一個合理的範圍之內, 當雜湊表儲存的鍵值對數量太多或者太少時, 程式需要對雜湊表的大小進行相應的擴充套件或者收縮。
擴充套件和收縮雜湊表的工作可以通過執行 rehash (重新雜湊)操作來完成, Redis 對字典的雜湊表執行 rehash 的步驟如下:
- 為字典的
ht[1]
雜湊表分配空間, 這個雜湊表的空間大小取決於要執行的操作, 以及ht[0]
當前包含的鍵值對數量 (也即是ht[0].used
屬性的值):- 如果執行的是擴充套件操作, 那麼
ht[1]
的大小為第一個大於等於ht[0].used * 2
的 2^n (2 的 n 次方冪); - 如果執行的是收縮操作, 那麼
ht[1]
的大小為第一個大於等於ht[0].used
的 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 做準備,下面會對照程式碼解讀
【注】:為什麼雜湊表的大小一定要是2的n次方呢?
- 減小雜湊衝突概率:
如果len是2的N次方,那麼len-1的後N位二進位制一定是全1,不同hashcode的key計算出來的陣列下標一定不同- 提高計算下標的效率:
如果len的二進位制後n位非全1,與len-1相與時,0與1相與需要取反,如果len為2的N次方,那麼與len-1相與,跟取餘len等價,而與運算效率高於取餘
假設程式要對圖 4-8 所示字典的 ht[0]
進行擴充套件操作, 那麼程式將執行以下步驟:
ht[0].used
當前的值為 4 , 4 * 2 = 8 , 而 8 (2^3)恰好是第一個大於等於 4 的 2 的 n 次方, 所以程式會將ht[1]
雜湊表的大小設定為 8 。 圖 4-9 展示了ht[1]
在分配空間之後, 字典的樣子。- 將
ht[0]
包含的四個鍵值對都 rehash 到ht[1]
, 如圖 4-10 所示。 - 釋放
ht[0]
,並將ht[1]
設定為ht[0]
,然後為ht[1]
分配一個空白雜湊表,如圖 4-11 所示。
雜湊表的擴充套件與收縮
當以下條件中的任意一個被滿足時, 程式會自動開始對雜湊表執行擴充套件操作:
- 伺服器目前沒有在執行
BGSAVE
命令或者BGREWRITEAOF
命令, 並且雜湊表的負載因子大於等於1
; - 伺服器目前正在執行
BGSAVE
命令或者BGREWRITEAOF
命令, 並且雜湊表的負載因子大於等於5
;
其中雜湊表的負載因子可以通過公式:
# 負載因子 = 雜湊表已儲存節點數量 / 雜湊表大小
load_factor = ht[0].used / ht[0].size
根據 BGSAVE
命令或 BGREWRITEAOF
命令是否正在執行, 伺服器執行擴充套件操作所需的負載因子並不相同, 這是因為在執行 BGSAVE
命令或 BGREWRITEAOF
命令的過程中, Redis 需要建立當前伺服器程序的子程序, 而大多數作業系統都採用寫時複製(copy-on-write)技術來優化子程序的使用效率, 所以在子程序存在期間, 伺服器會提高執行擴充套件操作所需的負載因子, 從而儘可能地避免在子程序存在期間進行雜湊表擴充套件操作, 這可以避免不必要的記憶體寫入操作, 最大限度地節約記憶體
另一方面, 當雜湊表的負載因子小於 0.1 時, 程式自動開始對雜湊表執行收縮操作
漸進式 Rehash
擴充套件或收縮雜湊表需要將 ht[0] 裡面的所有鍵值對 rehash 到 ht[1] 裡面, 但是, 這個 rehash 動作並不是一次性、集中式地完成的, 而是分多次、漸進式地完成的
如果 ht[0] 裡只儲存著四個鍵值對, 那麼伺服器可以在瞬間就將這些鍵值對全部 rehash 到 ht[1] ; 但是, 如果雜湊表裡儲存的鍵值對數量不是四個, 而是四百萬、四千萬甚至四億個鍵值對, 那麼要一次性將這些鍵值對全部 rehash 到 ht[1] 的話, 龐大的計算量可能會導致伺服器在一段時間內停止服務
雜湊表漸進式 rehash 的詳細步驟:
- 為
ht[1]
分配空間, 讓字典同時持有ht[0]
和ht[1]
兩個雜湊表。 - 在字典中維持一個索引計數器變數
rehashidx
, 並將它的值設定為 0 , 表示 rehash 工作正式開始。 - 在 rehash 進行期間, 每次對字典執行新增、刪除、查詢或者更新操作時, 程式除了執行指定的操作以外, 還會順帶將
ht[0]
雜湊表在rehashidx
索引上的所有鍵值對 rehash 到ht[1]
, 當 rehash 工作完成之後, 程式將rehashidx
屬性的值增一。 - 隨著字典操作的不斷執行, 最終在某個時間點上,
ht[0]
的所有鍵值對都會被 rehash 至ht[1]
, 這時程式將rehashidx
屬性的值設為 -1 , 表示 rehash 操作已完成。
圖 4-12 至圖 4-17 展示了一次完整的漸進式 rehash 過程, 注意觀察在整個 rehash 過程中, 字典的 rehashidx
屬性是如何變化的
rehashidx
即為table
(hash表)的下標
因為在進行漸進式 rehash 的過程中, 字典會同時使用 ht[0]
和 ht[1]
兩個雜湊表, 所以在漸進式 rehash 進行期間, 字典的刪除(delete)、查詢(find)、更新(update)等操作會在兩個雜湊表上進行: 比如說, 要在字典裡面查詢一個鍵的話, 程式會先在 ht[0]
裡面進行查詢, 如果沒找到的話, 就會繼續到 ht[1]
裡面進行查詢
在漸進式 rehash 執行期間, 新新增到字典的鍵值對一律會被儲存到 ht[1]
裡面, 而 ht[0]
則不再進行任何新增操作: 這一措施保證了 ht[0]
包含的鍵值對數量會只減不增, 並隨著 rehash 操作的執行而最終變成空表
通過原始碼解讀字典API
接下來將通過Redis5.0原始碼對上述操作進行解讀
新建一個雜湊表:
//dict.c
/* Reset a hash table already initialized with ht_init().
* NOTE: This function should only be called by ht_destroy(). */
static void _dictReset(dictht *ht)
{
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}
/* Create a new hash table */
dict *dictCreate(dictType *type,
void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type,privDataPtr);
return d;
}
/* Initialize the hash table */
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;
}
新增一個元素到雜湊表:
/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
/* Low level add or find:
* This function adds the entry but instead of setting a value returns the
* dictEntry structure to the user, that will make sure to fill the value
* field as he wishes.
*
* This function is also directly exposed to the user API to be called
* mainly in order to store non-pointers inside the hash value, example:
*
* entry = dictAddRaw(dict,mykey,NULL);
* if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
*
* Return values:
*
* If key already exists NULL is returned, and "*existing" is populated
* with the existing entry if existing is not NULL.
*
* If key was added, the hash entry is returned to be manipulated by the caller.
*/
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
//判斷 dict 是否正在擴容,如果正在擴容則再嘗試步長為1的擴容
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently. */
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
從hash表刪除一個元素:
/* Search and remove an element. This is an helper function for
* dictDelete() and dictUnlink(), please check the top comment
* of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t h, idx;
dictEntry *he, *prevHe;
int table;
if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
//判斷 dict 是否正在擴容,如果正在擴容則再嘗試步長為1的擴容
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
d->ht[table].used--;
return he;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return NULL; /* not found */
}
/* Remove an element, returning DICT_OK on success or DICT_ERR if the
* element was not found. */
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
/* Remove an element from the table, but without actually releasing
* the key, value and dictionary entry. The dictionary entry is returned
* if the element was found (and unlinked from the table), and the user
* should later call `dictFreeUnlinkedEntry()` with it in order to release it.
* Otherwise if the key is not found, NULL is returned.
*
* This function is useful when we want to remove something from the hash
* table but want to use its value before actually deleting the entry.
* Without this function the pattern would require two lookups:
*
* entry = dictFind(...);
* // Do something with entry
* dictDelete(dictionary,entry);
*
* Thanks to this function it is possible to avoid this, and use
* instead:
*
* entry = dictUnlink(dictionary,entry);
* // Do something with entry
* dictFreeUnlinkedEntry(entry); // <- This does not need to lookup again.
*/
dictEntry *dictUnlink(dict *ht, const void *key) {
return dictGenericDelete(ht,key,1);
}
/* You need to call this function to really free the entry after a call
* to dictUnlink(). It's safe to call this function with 'he' = NULL. */
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
if (he == NULL) return;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
Rehash的核心函式:
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
/*Returns 1 if there are still keys to move from the old to the new hash table,
*otherwise 0 is returned*/
// 最大訪問空桶數量,進一步減小可能引起阻塞的時間。
int empty_visits = n*10; /* Max number of empty buckets to visit. */
//d->rehashidx == -1時,沒有進行rehash時直接返回
if (!dictIsRehashing(d)) return 0;
//n為每次遷移的步長,擴容時,每次只移動 n 個元素,防止 redis 阻塞
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[