《Redis設計與實現》閱讀筆記(四)--字典
字典
字典,map,是用於保存鍵值對的抽象數據結構,是hash表實現。字典中的鍵唯一,通過鍵來操作值。Redis的數據庫使用字典來作為底層實現。
定義
Redis的字典使用哈希表作為底層實現,一個哈希表裏面由多個哈希表節點,哈希表節點保存著鍵值對。
哈希表
哈希表結構定義包含:哈希表數組,哈希表大小,哈希表掩碼,哈希表已有節點數。
1 typedef struct dictht { 2 dicEntry **table; 3 unsigned long size; 4 unsigned long sizemask; 5 unsigned long used;6 }dictht;
table就是哈希表數組,每個元素就是一個哈希表節點的指針。
size記錄了哈希表的大小,也就是table的大小。
sizemask是哈希表的掩碼,用於計算索引值,值總是size-1。
used就是哈希表已有節點數,註意與size進行區分哈希表大小並不等於節點數。
哈希表節點
哈希表節點結構包含:鍵,值,下一哈希表節點的指針(用於解決沖突)
1 typedef struct dictEntry { 2 void *key 3 union { 4 void *val 5 uint64_t u64; 6 int64_t s64;7 } v; 8 struct dictEntry *next; 9 } dictEntry;
next指針將哈希值相同的鍵值對連接在一起,形成鏈表,解決沖突。
字典
字典包含:一個大小為2的哈希表數組(方便rehash),rehash索引,特定類型的函數,復制鍵的函數。
1 typedef struct dict { 2 dictType *type; 3 void *privdata; 4 dictht ht[2]; 5 int trehashidx; 6 } dict;
type 和privdata的作用具體並不清楚,書上介紹“針對不同類型的鍵值對,為創建多態字典而設置”。
ht即hashtable平時的哈希表節點在ht[0],rehash的時候用到ht[1]。
哈希方式
插入鍵值對時,先根據鍵計算出哈希值。再根據哈希值和哈希表的掩碼計算出應放在哈希表的哪個索引上。哈希函數為Murmurhash算法,然而並不知道具體是怎麽個實現,給自己挖個坑,有空學習下。
hash = dict->type->hashFunction(key);
index = hash & dict->ht[x].sizemask;(x為0或1,取決於實際情況)
提到哈希肯定要考慮沖突的解決方法。在上面的哈希表節點中就已經看到,在這裏使用鏈地址法解決沖突。
單向鏈表而且沒有記錄尾節點,所以插入鏈表時使用頭插入。如果插入到末尾的話還需要遍歷到鏈表末尾,消耗時間。
rehash
負載因子 = 哈希表已保存的節點數量/哈希表大小
很顯然負載因子大說明哈希表太小,為了避免沖突就要增大。而負載因子太小說明哈希表過大,浪費了空間。
我們要對哈希表進行擴展或者收縮。這個工作通過rehash來完成。
- 為ht[1]分配空間。
- 將ht[0]的鍵值對rehash到ht[1]
- 當ht[0]全部遷移到ht[1],釋放ht[0],讓ht[1]成為新ht[0]
從上面的過程中我們發現有三個問題:
- 在什麽情況下我們需要對哈希表進行擴展或收縮?
- 新空間分配策略是怎樣的?
- rehash遷移的過程如何完成?
第一個問題:if 負載因子大於1且沒有執行BGSAVE,BGREEWRITEAOF 或者 負載因子大於5 (具體為啥這麽定我也不知道。。。第二個坑)
執行擴展
else if 負載因子小於0.1
執行收縮
第二個問題:擴展的大小為:大於等於(ht[0].used*2)的最小2的整數冪
縮小的大小為:大於等於(ht[0].used)的最小2的整數冪
第三個問題:漸進式rehash
rehash的過程為了不堵住服務,將分成幾次完成。
- 為ht[1]分配空間,將rehashidx設為0(不進行rehash的時候值為-1)
- 每次對字典進行增刪查改,程序將同時將rehashidx索引上的所有鍵值對rehash到ht[1] ,完成後rehashidx加一。
- 當ht[0]上的所有鍵值對全部完成rehash,將rehashidx設為-1。rehash完成
在rehash過程中,字典的刪查改將在兩個hashtable上進行,而增加操作只會在ht[1]進行。
《Redis設計與實現》閱讀筆記(四)--字典