除了用來表示數據庫以外,字典也是哈希鍵的底層實現redis
typedef struct dictEntry { void *key; //鍵 union { //值 void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; //指向下個哈希表節點,造成鏈表 } dictEntry; 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; /* 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; //哈希表大小 unsigned long sizemask; //哈希表大小掩碼,用於計算索引值,總數等於size -1 unsigned long used; //該哈希表已有節點的數量 } dictht; typedef struct dict { dictType *type; //類型特定函數 void *privdata; //私有數據 dictht ht[2]; //哈希表 // rehash 索引 //當rehash不在進行時,值爲-1 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ int iterators; /* number of iterators currently running */ } dict;
當有兩個或以上數量的鍵被分配到了哈希表數組的同一個索引上面時,咱們稱這些鍵發生了衝突(collision).
Redis的哈希表使用鏈地址法(separate chaining)來解決鍵衝突, 每一個哈希表節點都有一個next指針,多個哈希表節點能夠用next指針構成一個單向鏈表,被分配到同一個索引上的多個節點能夠用這個單向鏈表鏈接 起來,這就解決了鍵衝突的問題。
Rehash
隨着操做的不斷執行,哈希表保存的鍵值對會逐漸的增多或者減小,爲了讓哈希表的負載因子(load factor)維持在一個合理的範圍以內,當哈希表 保存的鍵值對數量太多或者太少時,程序須要對哈希表的大小進行相應的擴展或者收縮。
哈希表的擴展與收縮
算法
當如下條件中的任意一個被知足時,程序會自動開始對哈希表執行擴展操做。
1) 服務器目前沒有在執行BGSAVE命令或者BGREWRITEAOF命令,而且哈希表的負載因子大於等於1.
2)服務器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,而且哈希表的負載因子大於等於5
其中哈希表的負載因子能夠經過公式:
# 負載因子 = ht[0].used/ ht[0].size
計算出。
例如, 對於一個大小爲4,包含4個鍵值對的哈希表來講,這個哈希表的 負載因子爲: load_factor = 4/4 = 1.
又例如: 對於一個大小爲512,包含256個鍵值對的哈希表來講,
這個哈希表的負載因子爲: load_factor = 256/512 = 0.5
根據BGSAVE命令或BGREWRITEAOF命令是否正在執行,服務器執行擴展操做所需的負載因子並不相同,這是由於在執行BGSAVE命令或BGREWRITEAOF命令的過程 中,Redis須要建立當前服務器進程的子進程,而大多數操做系統都採用寫
時複製(copy-on-write)技術來優化子進程的使用效率,因此在子進程存在 期間,服務器會提升執行擴張操做所需的負載因子,從而儘量地避免在子 進程存在期間進行哈希擴展操做,這能夠避免沒必要要的內存寫入操做, 最大限度地節約內容。
另外一方面,當哈希表的負載因子小於0.1時,程序自動開始對哈希表進行收縮 操做。
漸進式Rehash:
爲了不rehash對服務器性能形成影響,服務器不是一次性將ht[0]裏面的全部鍵值對所有rehash到ht[1],而是分屢次,漸進式地將ht[0]裏面的鍵值對慢慢地rehash 到ht[1]。
哈希表漸進式rehash的詳細步驟:
1) 爲ht[1] 分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。
2) 在字典中維持一個索引計數器變量rehashidx,並將它的值設置爲0,表示rehash 工做正式開始。
3) 在rehash進行期間,每次對字典執行添加,刪除,查找或者更新操做時,程序除了 執行指定額操做之外,還會順帶將 ht[0] 哈希表在rehashidx索引上的全部鍵值對rehash到ht[1],當rehash工做完成以後,程序將rehashidx屬性的值增一。
4) 隨着字典操做的不斷執行,最終在某個時間點上,ht[0]的全部鍵值對都會被rehash到ht[1],這時程序將rehashidx屬性的值設爲-1,表示rehash操做完成。
漸進式rehash的好處在於它採用分而治之的方式,將rehash鍵值對所需的計算工做均攤 到對字典的每一個添加,刪除,查找和更新操做上,從而避免了集中式rehash而帶來的龐大計算量。
漸進式rehash執行期間的哈希表操做
由於在進行漸進式rehash的過程當中,字典會同時使用ht[0] 和ht[1] 兩個哈希表,
因此在漸進式rehash進行期間,字典的刪除(delete),查找(find), 更新(update)等操做會在兩個哈希表上進行。
例如:要在字典裏面查找一個鍵的話,程序會先在ht[0]裏面進行查找,
若是沒有找到的話,就會繼續在ht[1] 裏面進行查找,諸如此類。
另外,在漸進式rehash執行期間,新添加到字典的鍵值對一概會被保存到ht[1]裏面,而ht[0]則再也不進行任何添加操做,這一措施保證了ht[0]包含的鍵值對數量會只減不增,並隨着rehash操做的執行而最終變成空表。
數據庫
總結:
字典被普遍用於實現redis的各類功能,其中包括數據庫和哈希鍵。
redis中的字典使用哈希表做爲底層實現,每一個字典代用兩個哈希表,一個平時使用, 另外一個僅在進行rehash時使用。數組
當字典被用做數據庫的底層實現,或者哈希鍵的底層實現時,Redis使用MurmurHash2算法來計算鍵的哈希值。服務器
哈希表使用鏈地址法來解決鍵衝突,被分配到同一個索引上的多個鍵值對會鏈接成一個單向鏈表。函數
在對哈希表進行擴展或者收縮操做時,程序須要將現有哈希表包含的全部鍵值對rehash到新哈希表裏面,而且這個rehash過程並非一次性完成的,而是漸進式地 完成的。性能
相關資料:優化