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