摘要:屬性記錄了哈希表目前已有節點鍵值對的數量。字典字典的結構類型特定函數私有數據哈希表兩個記錄進度的標志。此外,字典在進行時,刪除查找更新等操作會在兩個哈希表上進行。在對哈希表進行擴容或收縮操作時,使用漸進式完成。
字典,是一種用于保存鍵值對的抽象數據結構。由于 C 語言沒有內置字典這種數據結構,因此 Redis 構建了自己的字典實現。
在 Redis 中,就是使用字典來實現數據庫底層的。對數據庫的 CURD 操作也是構建在對字典的操作之上。
除了用來表示數據庫之外,字典還是哈希鍵的底層實現之一。當一個哈希鍵包含的鍵值對比較多,又或者鍵值對中的元素都是比較長的字符串時,Redis 就會適應字典作為哈希鍵的底層實現。
1 字典的實現Redis 的字典使用哈希表作為底層實現。一個哈希表里面可以有多個哈希表節點,而每個哈希表節點就保存了字典中的一個鍵值對。
1.1 哈希表Redis 字典所使用的哈希表結構:
typedef struct dictht { dictEntry **table; // 哈希表數組 unsigned long size; // 哈希表大小 unsigned long sizemask; // 哈希表大小掩碼,用來計算索引 unsigned long used; // 哈希表現有節點的數量 } dictht;
table 屬性是一個數組。數組中的每個元素都是一個指向 dictEntry 結構的指針,每個 dictEntry 結構保存著一個鍵值對。
size 屬性記錄了哈希表的大小,也即是 table 數組的大小。
used 屬性記錄了哈希表目前已有節點(鍵值對)的數量。
sizemask 屬性的值總數等于 size-1,這個屬性和哈希值一起決定一個鍵應該被放到 table 數組中哪個索引上。
圖 1 展示了一個大小為 4 的空哈希表。
1.2 哈希表節點哈希表節點使用 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 屬性則保存著值。
next 屬性是指向另一個哈希表節點的指針。這個指針可以將多個哈希值相同的鍵值對連接在一起,以此來解決鍵沖突的問題。
圖 2 展示了通過 next 指針,將兩個索引相同的鍵 k1 和 k0 連接在一起的情況。
1.3 字典字典的結構:
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 屬性是針對不同類型的鍵值對,為創建多態字典而設置的。其中:
type 屬性是一個指向 dictType 結構的指針,每個 dictType 結構保存了一簇用于操作特定類型鍵值對的函數。Redis 會為用途不用的字典設置不同的類型特定函數。
privdata 屬性保存了需要傳給那些類型特定函數的可選參數。
而 ht 屬性是一個包含兩個哈希表的數組。一般情況下,字典只使用 ht[0],只有在對 ht[0] 進行 rehash 時才會使用 ht[1]。
rehashidx 屬性,它記錄了 rehash 目前的進度,如果當前沒有進行 rehash,它的值為 -1。至于什么是 rehash,別急,后面會詳細說明。
圖 3 是沒有進行 rehash 的字典:
2 插入算法當在字典中添加一個新的鍵值對時,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:
2.1 鍵沖突當有兩個或以上數量的鍵被分配到了哈希表數組的同一個索引上面時,我們認為這些鍵發生了建沖突。
Redis 的哈希表使用鏈地址法來解決建沖突。每個哈希表節點都有一個 next 指針,多個哈希表節點可以用 next 指針構成一個單向鏈表,被分配到同一個索引的多個節點用 next 指針鏈接成一個單向鏈表。
舉個栗子,假設我們要把 [k2, v2] 鍵值對添加到圖 6 所示的哈希表中,并且計算得出 k2 的索引值為 2,和 k1 沖突,因此,這里就用 next 指針將 k2 和 k1 所在的節點連接起來,如圖 7。
3 rehash 與 漸進式 rehash隨著對字典的操作,哈希表報錯的鍵值對會逐漸增多或者減少,為了讓哈希表的負載因子維持在一個合理的范圍之內,當哈希表報錯的鍵值對數量太多或者太少時,程序需要對哈希表進行相應的擴容或收縮。這個擴容或收縮的過程,我們稱之為 rehash。
對于負載因子,可以通過以下公式計算得出:
# 負載因子 = 哈希表已保存節點數量 / 哈希表大小 load_factor = ht[0].used / ht[0].size;3.1 哈希表的擴容與收縮
擴容
對于哈希表的擴容,源碼如下:
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); }
當以下條件被滿足時,程序會自動開始對哈希表執行擴展操作:
服務器當前沒有進行 rehash;
哈希表已保存節點數量大于哈希表大小;
dict_can_resize 參數為 1,或者負載因子大于設定的比率(默認為 5);
收縮
哈希表的收縮,源碼如下:
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%),會對哈希表進行收縮操作。
3.2 rehash擴容和收縮哈希表都是通過執行 rehash 操作來完成,哈希表執行 rehash 的步驟如下:
為字典的 ht[1] 哈希表分配空間,這個哈希表的空間大小取決于要執行的操作,以及 ht[0] 當前包含的鍵值對數量。
如果執行的是擴容操作,那么 ht[1] 的大小為**第一個大于等于 ht[0].usedx2 的 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 做準備。
示例:
假設程序要對圖 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。
3.3 漸進式 rehash對于 Redis 的 rehash 而言,并不是一次性、集中式的完成,而是分多次、漸進式地完成,所以也叫漸進式 rehash。
之所以采用漸進式的方式,其實也很好理解。當哈希表里保存了大量的鍵值對,要一次性的將所有鍵值對全部 rehash 到 ht[1] 里,很可能會導致服務器在一段時間內只能進行 rehash,不能對外提供服務。
因此,為了避免 rehash 對服務器性能造成影響,Redis 分多次、漸進式的將 ht[0] 里面的鍵值對 rehash 到 ht[1]。
漸進式 rehash 就用到了索引計數器變量 rehashidx,詳細步驟如下:
為 ht[1] 分配空間,讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。
在字段中維持一個索引計數器變量 rehashidx,并將它的值設置為 0,表示開始 rehash。
在 rehash 期間,每次對字典執行 CURD 操作時,程序除了執行指定的操作外,還會將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對移動到 ht[1],當 rehash 完成后,程序將 rehashidx 的值加一。
隨著不斷操作字典,最終在某個時間點上,ht[0] 的所有鍵值對都會被 rehash 到 ht[1],這時程序將 rehashidx 屬性的值設為 -1,表示 rehash 已完成。
漸進式 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 執行完畢
總結字段被廣泛用于實現 Redis 的各種功能,其中包括數據庫和哈希鍵。
Redis 中的字典使用哈希表作為底層實現,每個字典帶有兩個哈希表,一個平時使用,一個僅在 rehash 時使用。
哈希表使用鏈地址法來解決鍵沖突,被分配到同一個索引上的多個鍵值對會連接成一個單向鏈表。
在對哈希表進行擴容或收縮操作時,使用漸進式完成 rehash。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/62129.html
摘要:哈希對象哈希對象的可選編碼分別是和。編碼的哈希對象編碼的哈希對象使用壓縮列表作為底層實現。關于哈希編碼轉換的函數,可以參考,源碼如下是原始對象,是目標編碼。對應源碼如下對象元素數量為,或者總結哈希對象有和編碼。 繼續擼我們的對象和數據類型。 上節我們一起認識了字符串和列表,接下來還有哈希、集合和有序集合。 1 哈希對象 哈希對象的可選編碼分別是:ziplist 和 hashtable。...
摘要:對象源碼結構如下對象類型對象編碼引用統計指向底層實現數據結構的指針字段對象類型,就是我們常說的。。對象編碼對應跳躍表壓縮列表集合動態字符串等八種底層數據結構。 相信很多人應該都知道 Redis 有五種數據類型:字符串、列表、哈希、集合和有序集合。但這五種數據類型是什么含義?Redis 的數據又是怎樣存儲的?今天我們一起來認識下 Redis 這五種數據結構的含義及其底層實現。 首先要明確...
摘要:沒有直接使用語言傳統的字符串表示以空字符串結尾的字符數組,而是構建了一種名為簡單動態字符串的抽象類型,并將用作的默認字符串表示。對比字符串,有幾大優點常數復雜度獲取字符串長度杜絕緩沖區溢出減少修改字符串時所需的內存重分配次數。 Redis 沒有直接使用 C 語言傳統的字符串表示(以空字符串結尾的字符數組),而是構建了一種名為簡單動態字符串(simple dynamic string)的...
閱讀 3056·2021-11-25 09:43
閱讀 1651·2021-11-24 11:15
閱讀 2372·2021-11-22 15:25
閱讀 3520·2021-11-11 16:55
閱讀 3256·2021-11-04 16:10
閱讀 2787·2021-09-14 18:02
閱讀 1697·2021-09-10 10:50
閱讀 1083·2019-08-29 15:39