Redis中的字典
原文連結:https://www.changxuan.top/?p=1122
簡介
字典是一種在 Redis 中高頻使用的用於儲存鍵值對的抽象資料結構,在 Java 中常用的有 HasmMap
等。
由於字典中鍵的唯一性,所以在 Redis 中得到了廣泛的應用。
實現
Redis 中的字典是基於雜湊表
(dictht, dict hash table)實現的,雜湊表中的每個節點儲存一個鍵值對。雜湊表的結構體定義如下:
typedefstructdictht{
//雜湊表陣列
dictEntry**table;
//雜湊表大小
unsignedlongsize;
//雜湊表大小掩碼,用於計算索引值size-1,用來計算鍵值對放在哪個索引上
unsignedlongsizemask;
//雜湊表已有節點的數量
unsignedlongused;
}dictht;
雜湊表節點 dictEntry
的結構則如下所示:
typedefstructdictEntry{
//鍵
void*key;
//值
union{
void*val;
uint64_tu64;
int64_ts64;
}v;
//指向下個雜湊表節點
structdictEntry*next;
}dictEntry;
dictEntry
中的值有些特別,它表示其值有可能是一個指標或者是一個 uint64_t
整數,或者是一個 int64_t
整數。
因為存在 next
屬性,很顯然它是使用鏈地址法
解決的雜湊鍵衝突。
接下來我們看一下字典(dict)的定義:
typedefstructdict{
//型別特定函式
dictType*type;
//私有資料
void*privdata;
//雜湊表
dicththt[2];
//rehash索引當不在進行rehash的時候,值為-1
inttrehashids;
}dict;
屬性 type
是一個指向 dictType
結構體的指標,每個 dictType
儲存了一些用於操作特定型別鍵值對的函式。
typedefstructdictType{
//計算雜湊值的函式
unsignedint(*hashFunction)(constvoid*key);
//複製鍵的函式
void*(*keyDup)(void *privdata,constvoid*key);
//複製值的函式
void*(*valDup)(void*privdata,constvoid*obj);
//對比鍵的函式
int(*keyCompare)(void*privdata,constvoid*key1,constvoid*key2);
//銷燬鍵的函式
void(*valDestructor)(void*privdata,void*obj);
}dictType;
ht
陣列表示儲存兩個雜湊表,平常情況下只使用 ht[0]
,只有在 rehash
時才會使用到 h[1]
和 trehashids
。
字典的結構就是,一個字典中有兩個雜湊表,平時只用一個雜湊表。另一個雜湊表在 rehash 的時候使用。每個雜湊表中存在一個節點陣列,節點則用於存放鍵值對。
新增鍵值對
新增鍵值對就意味著需要計算鍵的雜湊值,從而得出索引值。根據索引值將鍵值對的雜湊節點放到雜湊表的指定位置上。計算雜湊值使用的是字典結構體中的 type
中的函式,即 hash = dict->type->hashFunction(key)
。計算索引值則是 index = hash & dict->ht[x].sizemask
,x 取決於當前使用的是ht[1]還是ht[2]。
不過,總會有不同的鍵對應相同的索引值,產生衝突。Redis 中使用了常用的“鏈地址法”來解決這個問題,當出現衝突時就把新節點放到表頭的位置。
Rehash
隨著字典中鍵值對數量的不斷變化,為了保證雜湊表的空間利用率以及效率,在雜湊表過大或者過小是要對雜湊表大小進行調整。如果過小,則會不斷髮生鍵衝突導致效率低下,如果過大則會浪費儲存空間。所以,經過不斷調整可以使其維持在一個合理的範圍。
步驟
為 ht[1]
分配空間,大小取決於是擴大雜湊表還是縮小雜湊表。如果擴大,其大小為第一個大於等於ht[0].used * 2
且同時為2的n次方冪
的值。如果縮小,其大小為第一個大於等於ht[0].used
其同時為2的n次方冪
的值。將儲存在 ht[0]
中所有的鍵值對重新計算雜湊值和索引值後,存放在ht[1]
中。當遷移完所有的鍵值之後,釋放原 ht[0]
的空間,將原h[1]
改為h0
, 並在ht[1]
新建立一個空白雜湊表。
那麼何時擴充套件雜湊表大小呢? 一是當沒有在執行 BGSAVE
或者 BGREWRITEAOF
命令時,並且雜湊表的負載因子大於等於1時。 二是當在執行這倆命令,但是負載因子大於等於5時(節約記憶體,上述兩命令消耗記憶體)。
負載因子計算公式為:負載因子 = 雜湊表儲存節點數量/雜湊表大小
那麼何時縮小雜湊表大小呢? 當雜湊表負載因子小於 0.1 時則會進行縮小。
漸進式 Rehash
其實對於上述步驟 2 ,普通人覺得這不就是把鍵值對重新分配一下嗎?但是如果此時存在百萬、千萬甚至億級的鍵值對時,恐怕就是不是一眨眼的功夫就可以完成的了。如果非得一次性完成,那麼可能會導致伺服器的不可用。所以為了解決這個問題,Redis 採用了慢慢來的辦法漸進式 Rehash。
其主要步驟與前面的有些相似,只不過在漸進式Rehash中使用到了 dict->trehashids
值來記錄當前rehash到了哪個索引。在 Rehash 期間,可以對字典正常進行增加、刪除、查詢和更新。然後同時也會將 trehashids
上記錄的索引值上的節點遷移到 h[1]
上。並且所有的新增節點都會放到 h[1]
中,這樣就會導致 h[0]
中的節點越來越少,最終完成 rehash。其它的操作則會在兩個表上進行。