Redis之字典

字典本身就是很常見的數(shù)據(jù)結(jié)構(gòu)之一,在Redis中尸饺,Redis數(shù)據(jù)庫就是使用字典來作為底層實(shí)現(xiàn)的进统,除了用來表示數(shù)據(jù)庫之外助币,字典還是哈希鍵的底層實(shí)現(xiàn)之一。

建議閱讀:
1螟碎、字典部分源碼研究見: wenmingxing Redis源碼研究之dict

I眉菱、字典的實(shí)現(xiàn)

Redis的字典使用哈希表作為底層實(shí)現(xiàn)。

1.1 哈希表

Redis字典所使用的哈希表結(jié)構(gòu)定義如下:

typedef struct dictht {

    // 哈希表數(shù)組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩碼掉分,用于計(jì)算索引值
    // 總是等于 size - 1
    unsigned long sizemask;

    // 該哈希表已有節(jié)點(diǎn)的數(shù)量
    unsigned long used;

} dictht;

table屬性是一個數(shù)組俭缓,數(shù)組中的每個元素都指向一個dictEntry結(jié)構(gòu)的指針,每個dictEntry結(jié)構(gòu)保存著一個鍵值對酥郭。

size屬性記錄了哈希表的大小华坦,即table數(shù)組的大小,而used屬性則記錄了哈希表目前已有鍵值對的數(shù)量不从。

sizemask屬性的值總是等于size-1惜姐,這個屬性和哈希值一起決定一個鍵應(yīng)該被放到table數(shù)組的哪個索引上。

下圖展示了一個大小為4的空哈希表:

1.2 哈希表節(jié)點(diǎn)

哈希表節(jié)點(diǎn)使用dictEntry結(jié)構(gòu)表示椿息,每個dictEntry結(jié)構(gòu)都保存一個鍵值對:

typedef struct dictEntry {

    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下個哈希表節(jié)點(diǎn)歹袁,形成鏈表
    struct dictEntry *next;

} dictEntry;

key屬性保持著鍵值對中的鍵,而v屬性則保存著鍵值對中的值寝优,其中鍵值對中的值可以是一個指針条舔,或者是一個整數(shù)。

next屬性是指向另一個哈希表節(jié)點(diǎn)的指針倡勇,這個指針可以將多個哈希值相同的鍵值對連接在一起逞刷,來解決鍵沖突問題(以鏈表的方式解決沖突問題)。

如下圖表示一個完成的哈希表:

1.3 字典

Redis中的字典結(jié)果如下:

typedef struct dict {

    // 類型特定函數(shù)
    dictType *type;

    // 私有數(shù)據(jù)
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 當(dāng) rehash 不在進(jìn)行時妻熊,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

type屬性和privdata屬性是針對不同類型的鍵值對,而創(chuàng)建多態(tài)字典而設(shè)置的:
type屬性是一個指向dictType結(jié)構(gòu)的指針仑最,每個dictType結(jié)構(gòu)保存了一組用于操作特定類型鍵值對的函數(shù)扔役,Redis會為用途不同的字典設(shè)置不同類型的特定函數(shù)。
而privadata屬性則保存了需要傳給那些類型特定函數(shù)的可選參數(shù)警医。

ht屬性是一個包含了兩個項(xiàng)的數(shù)組亿胸,數(shù)組中每個項(xiàng)都是一個dictht哈希表,一般情況下预皇,字典只使用ht[0]哈希表侈玄,而ht[1]哈希表只對ht[0]哈希表進(jìn)行rehash時使用。

另一個與rehash有關(guān)的就是rehashidx屬性吟温,它積累了rehash目前的進(jìn)度序仙,如果沒有進(jìn)行rehash,則它的值為-1鲁豪。

下圖為一個普通狀態(tài)下的字典結(jié)構(gòu):

II潘悼、哈希算法

將一個新的鍵值對添加到字典里面的時候律秃,程序需要先根據(jù)鍵值對上面的鍵來計(jì)算出哈希值和索引值,然后再根據(jù)索引值治唤,將包含新鍵值對的哈希表節(jié)點(diǎn)放到哈希數(shù)組的指定索引上面棒动。

Redis計(jì)算哈希值和索引值的方法如下:

# 使用字典設(shè)置的哈希函數(shù),計(jì)算鍵 key 的哈希值
hash = dict->type->hashFunction(key);

# 使用哈希表的 sizemask 屬性和哈希值宾添,計(jì)算出索引值
# 根據(jù)情況不同船惨, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

下面舉例說明一個完整的添加鍵值對<k0, v0>過程:

  1. 首先程序會先使用語句hash = dict->type->hashFunction(k0);計(jì)算的處k0的哈希值。
  2. 假設(shè)計(jì)算出的哈希值為8缕陕,則程序繼續(xù)index = hash & dict->ht[0].sizemask = 8 & 3 = 0;計(jì)算得到k0的索引值為0粱锐,這表示包含這個鍵值對的節(jié)點(diǎn)應(yīng)該放置到哈希表數(shù)組的索引0位置上。

tip: Redis使用MurmurHash2算法來計(jì)算鍵的哈希值榄檬。

III卜范、解決鍵沖突

Redis哈希表使用鏈地址法來解決鍵沖突,每個哈希表節(jié)點(diǎn)都有一個next指針鹿榜,多個哈希表節(jié)點(diǎn)可以用next構(gòu)成一個單向鏈表海雪,被分配到同一個索引上的節(jié)點(diǎn)可以用這個單向鏈表連接起來,從而解決鍵沖突問題舱殿。

下面的例子說明一個解決鍵沖突的實(shí)例:


另外因?yàn)閐ictEntry節(jié)點(diǎn)組成的鏈表沒有指向鏈表表尾的指針奥裸,為了考慮速度,程序總是將新節(jié)點(diǎn)添加到鏈表的表頭位置(這樣添加節(jié)點(diǎn)的時間復(fù)雜度為O(1))沪袭。

IV湾宙、rehash

隨著操作的不斷進(jìn)行,哈希表保存的鍵值對會逐漸增多或減少冈绊,為了讓哈希表負(fù)載因子維持在一個合理范圍之內(nèi)侠鳄,當(dāng)哈希表保存的鍵值對太多或太少時,程序要對哈希表的大小進(jìn)行相應(yīng)的擴(kuò)展或收縮死宣。

Redis對字典的哈希表執(zhí)行rehash的步驟如下:

  1. 為字典的ht[1]哈希表分配空間伟恶,這個空間大小取決于要執(zhí)行的操作:
    如果執(zhí)行的是擴(kuò)展操作,則ht[1]的大小為第一個大于等于等于ht[0].used*2的2^n毅该;
    如果執(zhí)行的收縮操作博秫,則ht[1]的大小為第一個大于等于ht[0].used的2^n;

  2. 將保存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash指的是重新計(jì)算鍵的哈希值和索引值眶掌,然后將鍵值對放置到ht[1]的指定位置上挡育。

  3. 當(dāng)ht[0]包含的所有鍵值對都遷移到ht[1]之后,釋放ht[0]朴爬,將ht[1]設(shè)置為ht[0]即寒,并在ht[1]新創(chuàng)建一個空白哈希表,為下一次rehash做準(zhǔn)備。

下面為一個rehash的實(shí)例 :


2*4 = 8(2^3):

重新計(jì)算索引蒿叠,并復(fù)制:

哈希表的擴(kuò)展與收縮
當(dāng)以下條件中任意一個被滿足時明垢,程序會自動開始對哈希表執(zhí)行擴(kuò)展操作:

  1. 服務(wù)器目前沒有執(zhí)行BGSAVE或BGREWRITEAOF命令,并且哈希表負(fù)載因子大于等于1市咽。
  2. 服務(wù)器正在執(zhí)行BGSAVE或BGREWRITEAOF命令痊银,并且哈希表負(fù)載因子大于等于5。

區(qū)分這兩種情況的目的在于施绎,因?yàn)閳?zhí)行BGSAVE與BGREWRITEAOF過程中溯革,Redis都需要創(chuàng)建子進(jìn)程,而大多數(shù)操作系統(tǒng)都采用寫時復(fù)制技術(shù)來優(yōu)化子進(jìn)程使用效率谷醉,所以在子進(jìn)程存在期間致稀,服務(wù)器會提高執(zhí)行擴(kuò)展操作所需的負(fù)載因子,從而盡可能避免在子進(jìn)程存在期間進(jìn)行哈希表擴(kuò)展操作俱尼,這可以避免不必要的內(nèi)存寫入抖单,最大限度的節(jié)約空間。

另一方面遇八,當(dāng)哈希表負(fù)載因子小于0.1時矛绘,程序自動開始對哈希表執(zhí)行收縮操作。

V刃永、漸進(jìn)式rehash

Redis中的rehash動作并不是一次性货矮、集中式完成的,而是分多次斯够、漸進(jìn)式的完成的囚玫。

這樣做的目的是,如果服務(wù)器中包含很多鍵值對读规,要一次性的將這些鍵值對全部rehash到ht[1]的話抓督,龐大的計(jì)算量可能導(dǎo)致服務(wù)器在一段時間內(nèi)停止服務(wù)于。

為了避免這種影響束亏,Redis采用了漸進(jìn)式Redis

  1. 為ht[1]分配空間本昏,讓字典同時持有ht[0]和ht[1]兩個哈希表。
  2. 在字典中維持一個索引計(jì)數(shù)器變量rehashidx枪汪,并將它置為0,表示rehash工作開始怔昨。
  3. 在rehash進(jìn)行期間雀久,每次對字典執(zhí)行添加、刪除趁舀、查找或者更新操作時赖捌,程序除了執(zhí)行指定操作以外,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1]中,當(dāng)rehash工作完成之后越庇,程序?qū)ehashidx屬性的值+1罩锐。
  4. 隨著字典操作的不斷進(jìn)行,最終在某個時間點(diǎn)上卤唉,ht[0]的所有鍵值對都被rehash到ht[1]上涩惑,這時將rehashidx屬性設(shè)為-1,表示rehash完成桑驱。

漸進(jìn)式rehash的好處在于其采取分而治之的方式竭恬,將rehash鍵值對所需要的計(jì)算工作均攤到字典的每個添加、刪除熬的、查找和更新操作上痊硕,從而避免了集中式rehash而帶來的龐大計(jì)算量。

下面為一個漸進(jìn)式rehash的實(shí)例:






漸進(jìn)式rehash執(zhí)行期間的哈希表操作
因?yàn)樵跐u進(jìn)式rehash的過程中押框,字典會同時使用ht[0]和ht[1]兩個哈希表岔绸,所以在漸進(jìn)式rehash進(jìn)行期間,字典的刪除橡伞、查找盒揉、更新等操作都是在兩個表上進(jìn)行的。

例如骑歹,查找操作會先在ht[0]上進(jìn)行预烙,如果沒找到再在ht[1]上進(jìn)行。

添加操作的鍵值對會一律保存到ht[1]中道媚,這一措施保證ht[0]包含的鍵值對只會減少不會增加扁掸。

VI、字典API


【參考】
[1] 《Redis的設(shè)計(jì)與實(shí)現(xiàn)》

歡迎轉(zhuǎn)載最域,轉(zhuǎn)載請注明出處wenmingxing Redis之字典

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谴分,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子镀脂,更是在濱河造成了極大的恐慌牺蹄,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薄翅,死亡現(xiàn)場離奇詭異沙兰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)翘魄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門鼎天,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人暑竟,你說我怎么就攤上這事斋射。” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵罗岖,是天一觀的道長涧至。 經(jīng)常有香客問我,道長桑包,這世上最難降的妖魔是什么南蓬? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮捡多,結(jié)果婚禮上蓖康,老公的妹妹穿的比我還像新娘。我一直安慰自己垒手,他們只是感情好蒜焊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著科贬,像睡著了一般泳梆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上榜掌,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天优妙,我揣著相機(jī)與錄音,去河邊找鬼憎账。 笑死套硼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胞皱。 我是一名探鬼主播邪意,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼反砌!你這毒婦竟也來了雾鬼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤宴树,失蹤者是張志新(化名)和其女友劉穎策菜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酒贬,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡又憨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锭吨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竟块。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖耐齐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤埠况,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布耸携,位于F島的核電站,受9級特大地震影響辕翰,放射性物質(zhì)發(fā)生泄漏夺衍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一喜命、第九天 我趴在偏房一處隱蔽的房頂上張望沟沙。 院中可真熱鬧,春花似錦壁榕、人聲如沸矛紫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颊咬。三九已至,卻和暖如春牡辽,著一層夾襖步出監(jiān)牢的瞬間喳篇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工态辛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留麸澜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓奏黑,卻偏偏與公主長得像炊邦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子攀涵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354