掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
Redis Hash(散列表)是一種 field-value pairs(鍵值對)集合類型,類似于 Python 中的字典、Java 中的 HashMap。一個 field 對應(yīng)一個 value,你可以通過 field 在 O(1) 時間復(fù)雜度查 field 找關(guān)聯(lián)的 field,也可以通過 field 來更新或者刪除這個鍵值對。

Redis 的散列表 dict 由數(shù)組 + 鏈表構(gòu)成,數(shù)組的每個元素占用的槽位叫做哈希桶,當出現(xiàn)散列沖突的時候就會在這個桶下掛一個鏈表,用“拉鏈法”解決散列沖突的問題。
簡單地說就是將一個 key 經(jīng)過散列計算均勻的映射到散列表上。
圖 2-18
Hash 數(shù)據(jù)類型底層存儲數(shù)據(jù)結(jié)構(gòu)實際上有兩種。
通常情況下使用 dict 數(shù)據(jù)結(jié)構(gòu)存儲數(shù)據(jù),每個 field-value pairs 構(gòu)成一個 dictEntry 節(jié)點來保存。
只有同時滿足以下兩個條件的時候,才會使用 listpack(7.0 版本之前使用 ziplist)數(shù)據(jù)結(jié)構(gòu)來代替 dict 存儲, 把 key-value 鍵值對按照 field 在前 value 在后,緊密相連的方式放到一次把每個鍵值對放到列表的表尾。
每次向散列表寫數(shù)據(jù)的時候,都會調(diào)用 t_hash.c 中的hashTypeConvertListpack()函數(shù)來判斷是否需要轉(zhuǎn)換底層數(shù)據(jù)結(jié)構(gòu)。
當插入和修改的數(shù)據(jù)不滿足以上兩個條件時,就把散列表底層存儲結(jié)構(gòu)轉(zhuǎn)換成 dict結(jié)構(gòu)。需要注意的是,不能由 dict 退化成 listpack。
雖然使用了 listpack 就無法實現(xiàn) O(1) 時間復(fù)雜度操作數(shù)據(jù),但是使用 listpack 能大大減少內(nèi)存占用,而且數(shù)據(jù)量比較小,性能并不是有太大差異。
為了對上層屏蔽散列表底層使用了不同數(shù)據(jù)結(jié)構(gòu)存儲,所以抽象了一個 hashTypeIterator 迭代器來實現(xiàn)散列表的查詢。
Hashes 數(shù)據(jù)類型使用 listpack 作為存儲數(shù)據(jù)時的情況,如圖 2-19 所示。
圖 2-19
listpack 數(shù)據(jù)結(jié)構(gòu)在之前的已經(jīng)介紹過, 接下來帶你揭秘 dict 到底長啥樣。
Redis 數(shù)據(jù)庫就是一個全局散列表。正常情況下,我只會使用 ht_table[0]散列表,圖 2-20 是一個沒有進行 rehash 狀態(tài)下的字典。
圖 2-20
dict 字典在源代碼 dict.h中使用 dict 結(jié)構(gòu)體表示。
struct dict {
dictType *type;
// 真正存儲數(shù)據(jù)的地方,分別存放兩個指針
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx;
int16_t pauserehash;
signed char ht_size_exp[2];
};繼續(xù)看 dictEntry,數(shù)組中每個元素都是 dictEntry 類型,就是這玩意存放了鍵值對,表示字典的一個節(jié)點。
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;MySQL:“為啥 ht_table[2] 存放了兩個指向散列表的指針?用一個散列表不就夠了么?!?/p>
默認使用 ht_table [0] 進行讀寫數(shù)據(jù),當散列表的數(shù)據(jù)越來越多的時候,哈希沖突嚴重會出現(xiàn)哈希桶的鏈表比較長,導(dǎo)致查詢性能下降。
我為了唯快不破想了一個法子,當散列表保存的鍵值對太多或者太少的時候,需要通過 rehash(重新散列)對散列表進行擴容或者縮容。
MySQL:“什么時候會觸發(fā)擴容?”
負載因子 = 散列表存儲 dictEntry 節(jié)點數(shù)量 / 散列表桶個數(shù)。完美情況下,每個哈希桶存儲一個 dictEntry 節(jié)點,這時候負載因子 = 1。
MySQL:“需要遷移數(shù)據(jù)量很大,rehash 操作豈不是會長時間阻塞主線程?”
為了防止阻塞主線程造成性能問題,我并不是一次性把全部的 key 遷移,而是分多次,將遷移操作分散到每次請求中,避免集中式 rehash 造成長時間阻塞,這個方式叫漸進式 rehash。
在執(zhí)行漸進式 rehash 期間,dict 會同時使用 ht_table[0] 和 ht_table[1]兩個散列表,rehash 具體步驟如下。
MySQL:“rehash 過程中,字典的刪除、查找、更新和添加操作,要從兩個 ht_table 都搞一遍么?”
刪除、修改和查找可能會在兩個散列表進行,第一個散列表沒找到就到第二個散列表進行查找。但是增加操作只會在新的散列表上進行。
MySQL:“如果請求比較少,豈不是會很長時間都要使用兩個散列表?!?/p>
好問題,在 Redis Server 初始化時,會注冊一個時間事件,定時執(zhí)行 serverCron 函數(shù),其中包含 rehash 操作用于輔助遷移,避免這個問題。
serverCron 函數(shù)除了做 rehash 以外,主要處理如下工作。
是不是很貼心,既能保證性能,又能避免內(nèi)存浪費。好了,今天散列表底層數(shù)據(jù)結(jié)構(gòu)實現(xiàn)原理就到這里。后面我將給大家分享如何使用 Hash 實現(xiàn)購物車功能。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流