前言字典, 又稱符號表(symbol table)、關(guān)聯(lián)數(shù)組(associative array)或者映射(map), 是一種用于保存鍵值對(key-value pair)的抽象數(shù)據(jù)結(jié)構(gòu)。 在字典中, 一個鍵(key)可以和一個值(value)進行關(guān)聯(lián)(或者說將鍵映射為值), 這些關(guān)聯(lián)的鍵和值就被稱為鍵值對。 字典中的每個鍵都是獨一無二的, 程序可以在字典中根據(jù)鍵查找與之關(guān)聯(lián)的值, 或者通過鍵來更新值, 又或者根據(jù)鍵來刪除整個鍵值對, 等等。 字典經(jīng)常作為一種數(shù)據(jù)結(jié)構(gòu)內(nèi)置在很多高級編程語言里面, 但 Redis 所使用的 C 語言并沒有內(nèi)置這種數(shù)據(jù)結(jié)構(gòu), 因此 Redis 構(gòu)建了自己的字典實現(xiàn)。 字典在 Redis 中的應(yīng)用相當(dāng)廣泛, 比如 Redis 的數(shù)據(jù)庫就是使用字典來作為底層實現(xiàn)的, 對數(shù)據(jù)庫的增、刪、查、改操作也是構(gòu)建在對字典的操作之上的。 因此,了解字典對我們了解Redis數(shù)據(jù)庫有很大的幫助。同時可以跟Java的HashMap進行對比,看看孰好孰壞。
字典的定義1 typedef struct dict { 2 3 // 類型特定函數(shù) 4 dictType *type; 5 6 // 私有數(shù)據(jù) 7 void *privdata; 8 9 // 哈希表 10 dictht ht[2]; 11 12 // rehash 索引 13 // 當(dāng) rehash 不在進行時,值為 -1 14 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ 15 16 } dict; 主要看ht,和rehashidx兩個參數(shù)。
除了
1 typedef struct dictht { 2 3 // 哈希表數(shù)組 4 dictEntry **table; 5 6 // 哈希表大小 7 unsigned long size; 8 9 // 哈希表大小掩碼,用于計算索引值 10 // 總是等于 size - 1 11 unsigned long sizemask; 12 13 // 該哈希表已有節(jié)點的數(shù)量 14 unsigned long used; 15 16 } dictht;
而
1 typedef struct dictEntry { 2 3 // 鍵 4 void *key; 5 6 // 值 7 union { 8 void *val; 9 uint64_t u64; 10 int64_t s64; 11 } v; 12 13 // 指向下個哈希表節(jié)點,形成鏈表 14 struct dictEntry *next; 15 16 } dictEntry;
可以明顯的看出來,redis是通過鏈表來解決hash沖突的。
因此,redis的字典大概如下:
Rehash隨著操作的不斷執(zhí)行, 哈希表保存的鍵值對會逐漸地增多或者減少, 為了讓哈希表的負載因子(load factor)維持在一個合理的范圍之內(nèi), 當(dāng)哈希表保存的鍵值對數(shù)量太多或者太少時, 程序需要對哈希表的大小進行相應(yīng)的擴展或者收縮。 也就是我們常說的,擴容,再次hash。 Redis rehash過程:
但其實rehash是非常的耗時間的。假設(shè)ht[0]非常的大呢? 40W,400W,甚至4000W呢? 一次rehash甚至可能導(dǎo)致redis宕機,所以出現(xiàn)了漸進式hash。
漸進式Rehash這個 rehash 動作并不是一次性、集中式地完成的, 而是分多次、漸進式地完成的。為了避免 rehash 對服務(wù)器性能造成影響, 服務(wù)器不是一次性將
擴容代碼大致如下: 1 int dictRehash(dict *d, int n) { 2 int empty_visits = n*10; /* Max number of empty buckets to visit. */ 3 4 // 判斷是否正在擴容 5 if (!dictIsRehashing(d)) return 0; 6 7 while(n-- && d->ht[0].used != 0) { 8 dictEntry *de, *nextde; 9 10 /* Note that rehashidx can't overflow as we are sure there are more 11 * elements because ht[0].used != 0 */ 12 assert(d->ht[0].size > (unsigned long)d->rehashidx); 13 14 // 找到一個不為空的桶,進行遷移 15 while(d->ht[0].table[d->rehashidx] == NULL) { 16 d->rehashidx++; 17 if (--empty_visits == 0) return 1; 18 } 19 // 找到這個桶第一個指針節(jié)點 20 de = d->ht[0].table[d->rehashidx]; 21 // 將這個桶中的所有的key節(jié)點轉(zhuǎn)移到新的數(shù)組中。while循環(huán)鏈表 22 while(de) { 23 uint64_t h; 24 25 nextde = de->next; 26 /* Get the index in the new hash table */ 27 h = dictHashKey(d, de->key) & d->ht[1].sizemask; 28 de->next = d->ht[1].table[h]; 29 d->ht[1].table[h] = de; 30 d->ht[0].used--; 31 d->ht[1].used++; 32 de = nextde; 33 } 34 // 舊的桶節(jié)點置為null,并且rehashidx+1 35 d->ht[0].table[d->rehashidx] = NULL; 36 d->rehashidx++; 37 } 38 39 /* Check if we already rehashed the whole table... */ 40 if (d->ht[0].used == 0) { 41 zfree(d->ht[0].table); 42 d->ht[0] = d->ht[1]; 43 _dictReset(&d->ht[1]); 44 d->rehashidx = -1; 45 return 0; 46 } 47 48 /* More to rehash... */ 49 return 1; 50 }
在進行漸進式 rehash 的過程中, 字典會同時使用 另外, 在漸進式 rehash 執(zhí)行期間, 新添加到字典的鍵值對一律會被保存到
所遇到問提問題一: 要在字典里面查找一個鍵的話, 程序會先在 這一點其實我比較有疑惑?為何要先去ht[0]中查找,找不到再去ht[1]中查找,這樣豈不是效率低下,查找了兩張表。不能根據(jù)hash值和rehashidx進行比較判斷么?只查一張表不行么? 為此我翻閱了不少資料: 這是redis官方其他人的pull request:https://github.com/antirez/redis/pull/5692 暫時還沒有merge 同時這是我和一位大佬的討論:https://github.com/Junnplus/blog/issues/35 最終得出結(jié)論 還是看源碼:(源碼真是一個好東西) 1 for (table = 0; table <= 1; table++) { 2 // 找到key的index 3 idx = h & d->ht[table].sizemask; 4 // 拿到key值對應(yīng)的value 5 he = d->ht[table].table[idx]; 6 while(he) { 7 if (key==he->key || dictCompareKeys(d, key, he->key)) 8 return he; 9 he = he->next; 10 } 11 if (!dictIsRehashing(d)) return NULL; 12 } 其實只有兩種情況
針對第一種情況,他在第一層的循環(huán)已經(jīng)找到了key值,并且返回(第八行),不再繼續(xù)后面操作,因此不存在效率問題。 第二種情況,看第五行,he此時為null,根本不會循環(huán)鏈表。然后第二次循環(huán)才能找到key。而第一次是做了一次hash,復(fù)雜度為O(1)。效率幾乎是沒有損失,因此也不存在效率問題。 綜上:我得出的結(jié)論是。可以拿idx和rehashidx進行對比,同時也可以像redis這樣寫,不會損失效率。redis可能為了代碼的簡潔以及統(tǒng)一,不想寫那么多的判斷條件,因此沒有比較idx和rehashidx。 當(dāng)我認為提前結(jié)束會更好,畢竟不用后續(xù)判斷了,也比較清楚。類似這個request:https://github.com/antirez/redis/pull/5692/files
問題二: 假如在redis準(zhǔn)備進行rehash的時候,他需要為ht[1]申請一塊內(nèi)存,這塊內(nèi)存可是ht[0]的兩倍哦,那次是計算機內(nèi)存不存會如何? 梳理一下哈希表大小和內(nèi)存申請大小的對應(yīng)關(guān)系: 正常狀態(tài)下,redis為ht[1]分配完內(nèi)存后,會持續(xù)一段時間進行rehash。rehash完畢后,就會釋放ht[0]內(nèi)存了。類似如下圖: 內(nèi)存先上升,后下降。
但此時內(nèi)存不存的話,Redis會進行大量的Key驅(qū)逐,也就是會淘汰掉很久未使用的key,跟LRU有點類似。 那么此時可能就會影響到了業(yè)務(wù),這是非常大的問題呢。 那么針對在Redis滿容驅(qū)逐狀態(tài)下,如何避免因Rehash而導(dǎo)致Redis抖動的這種問題。
參考文獻: 《redis設(shè)計與實現(xiàn)》 http:///preview/dict/incremental_rehashing.html 《美團針對Redis Rehash機制的探索和實踐》 https://tech.meituan.com/2018/07/27/redis-rehash-practice-optimization.html 《Redis源碼分析》 https://github.com/Junnplus/blog/issues/35
|
|
|