Redis字典的實現(xiàn)

字典

Redis 中的字典 由 dict.h/dict 結(jié)構(gòu)表示:

typedef struct dict {
    dictType *type;     //類型特定函數(shù)
    void *privdata;     //私有數(shù)據(jù)
    dictht ht[2];       //哈希表
    int rehashdx;      //rehash 索引,當 rehash 不在進行時,值為-1
} dict;

type 和 privdata 是針對不同類型的鍵值對,為創(chuàng)建多態(tài)字典而設(shè)置的
type指向一個 dictType 結(jié)構(gòu)的指針, 每個dictType 結(jié)構(gòu)保存了一簇用于操作特作特定類型鍵值對的函數(shù), Redis 會為用途不同的字典設(shè)置不同的類型特定函數(shù).

pridata 則保存了需要傳給那些特定類型函數(shù)看可選參數(shù).
ht 屬性,包含兩個數(shù)組,數(shù)組的每一項都是一個 dictht 哈希表,一般情況下字典只使用ht[0] 哈希表,ht[1]哈希表只會在對哈希表進行 rehash 時使用.
rehashidex 記錄了 rehash 當前的進度,如果沒有進行 rehash, 值就為-1.

哈希表

Redis 字典所使用的哈希表由 dict.h/dictht 結(jié)構(gòu)定義:

typedef struct dictht {
    dictEntry **table;      //哈希表數(shù)組
    unsigned long size;     //哈希表大小
    unsigned long sizemask; //用于計算索引值,
                            //總是等于 size - 1
    unsigned long used;     //哈希表已有節(jié)點數(shù)量
}  dictht;

table 屬性是一個數(shù)組, 數(shù)組中每個元素都是一個指向 dictEntry 結(jié)構(gòu)的指針, 每個 dictEntry 結(jié)構(gòu)保存著一個鍵值對.
size 屬性記錄了哈希表的大小,也就是 table 數(shù)組的大小
sizemask 屬性和哈希值一起決定一個鍵應(yīng)該被放到 table 數(shù)組的哪個索引上面

哈希表節(jié)點

哈希表節(jié)點使用 dictEntry 表示,每個 dictEntry 結(jié)構(gòu)保存著一個鍵值對

typedef struct dictEntry {
    void *key;          //鍵
    union {             //值
        void *val;
        uint_64 u64;
        int64_t s64;
    } v;                    
    sturct dictEntry *next; //指向下個哈希表節(jié)點,形成鏈表
} dictEntry;

注意這里 v 屬性保存著鍵值對中的值,其中的鍵值可以是指針,或是 uint_64 整數(shù),又或者是 int64_t 整數(shù).
next 屬性是指向另一個哈希表節(jié)點的指針,這個指針將多個哈希值相同的鍵值對連接在一起,以此來解決鍵值沖突(collision)問題.

下圖展示了一個普通狀態(tài)下的字典(沒有 rehash 進行)

哈希算法

Redis 使用 MurmurHash2 算法 來計算鍵的哈希值.
Redis 計算哈希值和索引的方法如下:
hash = dict->type->hashFunction(k);
index = hash & dict->ht[0].sizemask

解決鍵沖突

當有兩個或兩個以上的鍵被分配到了哈希表數(shù)組的同一索引上時,稱這些鍵發(fā)生了沖突( collision)

Redis 的哈希表使用鏈地址法來解決沖突,每個哈希表節(jié)點都有一個 next 指針,多個哈希表節(jié)點可以用 next 指針構(gòu)成一個單項鏈表,被分配到同一個索引上的多個節(jié)點可以用這個對單向鏈表連接起來,這就解決了鍵沖突的問題.

如前面的字典示意圖所示, 鍵 k0 和 k1 的索引值均為1,這里只需用 next 指針將兩個節(jié)點連接起來.
,因為dictEntry 節(jié)點組成的鏈表沒有表尾指針,為了 速度考慮,程序總是將新節(jié)點調(diào)價到鏈表的表頭位置,排在其他已有節(jié)點的前面,這樣插入的復雜度為$ O(1)$.

Rehash

隨著操作的不斷進行, 哈希表保存的鍵值對會逐漸地增多或減少,為了讓哈希表的負載因子維持在一個合理的范圍之內(nèi),當哈希表保存的鍵值對數(shù)量太多或者太少時, 程序需要對哈希表的大小進行相應(yīng)的擴展或者收縮.這個過程叫做rehash.

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

  1. 為字典的 ht[1] 哈希表分配空間,空間的大小取決于要執(zhí)行的操作,以及 ht0]當前包含的鍵值對數(shù)量( used 屬性值):
  • 如果執(zhí)行的是擴展操作,那么 ht[1] 的大小為第一個大于等于 ht0].used*2的 $2^n$ .
  • 如果執(zhí)行的是收縮操作,那么ht[1]的大小為打一個大于等于 ht[0].used 的$2^n$.
  1. 將保存在 ht[0] 中所有鍵值對 rehash 到 ht[1] 上面: 任何事指的是重新計算鍵的哈希值和索引值,然后鍵鍵值對放到 ht[1] 哈希表的指定位置.
  2. 當 ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之后, 釋放 ht[0], 再將 ht[1] 設(shè)置為 ht[0],并在 ht[1] 后面創(chuàng)建一個空白的哈希表.

舉個例子,假設(shè)程序要對下圖的 `ht[0] 進行擴展操作

ht[0].used 當前值為4 , $2^3$ 恰好是第一個大于等于 4*2 的值,所以 ht[1] 哈希表的大小設(shè)置為8,下圖展示了 ht[1] 分配了空間之后字典的樣子.

將 ht[0] 包含的四個鍵值對 rehash 到 ht[1], 圖下圖所示:

釋放 ht[0], 將 ht[1] 設(shè)置為 ht[0]. 再分配一個空哈希表. 哈希表的大小由原來的4 擴展至8.

漸進式 rehash

上一節(jié)說過, 擴展或收縮哈希表需要將 ht[0] 里的所有鍵值對 rehash 到 ht[1] 中,但是這個 rehash
動作并不是一次性,集中式完成的,而是分多次,漸進式完成的.

這么做的原因是,當哈希表里保存的鍵值對多至百萬甚至億級別時,一次性地全部 rehash 的話,龐大的計算量會對服務(wù)器性能造成嚴重影響.

以下是漸進式 rehash 的步驟:

  1. 為 ht[1] 分配空間
  2. 在字典中維持一個索引計數(shù)器變量 rehashidx, 將它的值設(shè)置為0,表示 rehash 正式開始
  3. 在 rehash 進行期間,每次對字典進行增刪改查時,順帶將 ht[0] 在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1] 中,同時將 rehashidx 加 1.
  4. 隨著操作不斷進行,最終在某個時間點上, ht[0] 所有的鍵值對全部 rehash 到 ht[1] 上,這是講 rehashidx 屬性置為 -1,,表示 rehash操作完成.
  5. 在漸進式 rehash 執(zhí)行期間,新添加到字典的鍵值對一律保存到 ht[1] 里,不會對 ht[0] 做添加操作,這一措施保證了 ht[0]只減不增,并隨著 rehash 進行, 最終編程空表.

Ref:
https://segmentfault.com/a/1190000004850844

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诈皿,一起剝皮案震驚了整個濱河市次绘,隨后出現(xiàn)的幾起案子衷掷,更是在濱河造成了極大的恐慌漠酿,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡灵汪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門柑潦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來享言,“玉大人,你說我怎么就攤上這事妒茬〉4福” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵乍钻,是天一觀的道長肛循。 經(jīng)常有香客問我,道長银择,這世上最難降的妖魔是什么多糠? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮浩考,結(jié)果婚禮上夹孔,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好搭伤,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布只怎。 她就那樣靜靜地躺著,像睡著了一般怜俐。 火紅的嫁衣襯著肌膚如雪身堡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天拍鲤,我揣著相機與錄音贴谎,去河邊找鬼。 笑死季稳,一個胖子當著我的面吹牛擅这,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播景鼠,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仲翎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了铛漓?” 一聲冷哼從身側(cè)響起谭确,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎票渠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芬迄,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡问顷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了禀梳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杜窄。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖算途,靈堂內(nèi)的尸體忽然破棺而出塞耕,到底是詐尸還是另有隱情,我是刑警寧澤嘴瓤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布扫外,位于F島的核電站洁段,受9級特大地震影響袱瓮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锐峭,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一停忿、第九天 我趴在偏房一處隱蔽的房頂上張望驾讲。 院中可真熱鬧,春花似錦、人聲如沸吮铭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谓晌。三九已至掠拳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扎谎,已是汗流浹背碳想。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毁靶,地道東北人胧奔。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像预吆,于是被迫代替她去往敵國和親龙填。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 字典拐叉,又稱符號表岩遗,是保存鍵值對的抽象數(shù)據(jù)結(jié)構(gòu)。很多語言都內(nèi)置字典這種常用的數(shù)據(jù)結(jié)構(gòu)凤瘦,但是C語言沒有內(nèi)置宿礁,所以red...
    舒小賤閱讀 1,316評論 0 2
  • Map 是一種很常見的數(shù)據(jù)結(jié)構(gòu)梆靖,用于存儲一些無序的鍵值對。在主流的編程語言中笔诵,默認就自帶它的實現(xiàn)返吻。C、C++ 中的...
    一縷殤流化隱半邊冰霜閱讀 9,251評論 23 67
  • 本文摘抄自redis閱讀筆記 典在Redis中應(yīng)用十分廣泛乎婿,它是實現(xiàn)數(shù)據(jù)庫的基礎(chǔ)测僵,特別的它是數(shù)據(jù)庫鍵空間的實現(xiàn)方式...
    lintong閱讀 596評論 0 3
  • 字典(dictionary), 又名映射(map)或關(guān)聯(lián)數(shù)組(associative array)谢翎,是一種抽象數(shù)據(jù)...
    待汝豪杰只是凡夫閱讀 331評論 0 1
  • 字典是Redis的重要數(shù)據(jù)結(jié)構(gòu)捍靠,Redis的數(shù)據(jù)庫就是使用字典作為底層實現(xiàn)的。代碼位于dict.h和dict.c中...
    童伯虎閱讀 3,011評論 2 3