「Redis設計與實現」字典篇

字典簡介

字典, 又稱符號表(symbol table)、關聯數組(associative array)或者映射(map), 是一種用于保存鍵值對(key-value pair)的抽象數據結構。
在字典中, 一個鍵(key)可以和一個值(value)進行關聯(或者說將鍵映射為值), 這些關聯的鍵和值就被稱為鍵值對。

字典中的每個鍵都是獨一無二的, 程序可以在字典中根據鍵查找與之關聯的值, 或者通過鍵來更新值, 又或者根據鍵來刪除整個鍵值對, 等等。

字典經常作為一種數據結構內置在很多高級編程語言里面, 但 Redis 所使用的 C 語言并沒有內置這種數據結構, 因此 Redis 構建了自己的字典實現。

字典在 Redis 中的應用相當廣泛, 比如 Redis 的數據庫就是使用字典來作為底層實現的, 對數據庫的增、刪、查、改操作也是構建在對字典的操作之上的。
如,當我們執行命令:

redis> SET msg "hello world"
OK

在數據庫中創建一個鍵為 "msg" , 值為 "hello world" 的鍵值對時, 這個鍵值對就是保存在代表數據庫的字典里面的。

字典的實現

Redis 的字典使用哈希表作為底層實現, 一個哈希表里面可以有多個哈希表節點, 而每個哈希表節點就保存了字典中的一個鍵值對。接下來的三個小節將分別介紹 Redis 的哈希表、哈希表節點、以及字典的實現。

  • 哈希表
typedef struct dictht {
    dictEntry **table; /* 哈希表數組 */
    unsigned long size; /* 哈希表大小 */
    unsigned long sizemask; /*哈希表大小掩碼,用于計算索引值 總是等于 size - 1*/
    unsigned long used; /* 該哈希表已有節點的數量 */
} dictht;
  1. table 屬性是一個數組, 數組中的每個元素都是一個指向 dict.h/dictEntry 結構的指針, 每個 dictEntry 結構保存著一個鍵值對。
  2. size 屬性記錄了哈希表的大小, 也即是 table 數組的大小, 而 used 屬性則記錄了哈希表目前已有節點(鍵值對)的數量。
  3. sizemask 屬性的值總是等于 size - 1 , 這個屬性和哈希值一起決定一個鍵應該被放到 table 數組的哪個索引上面。
  • 哈希表節點
typedef struct dictEntry {
    void *key; /* 鍵 */
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next; /* 指向下個哈希表節點,形成鏈表,hash鍵沖突時才被用到 */
} dictEntry;
  1. 哈希表節點使用dictEntry結構表示, 每個dictEntry結構都保存著一個鍵值對
  2. key屬性保存著鍵值對中的鍵, 而v屬性則保存著鍵值對中的值, 其中鍵值對的值可以是一個指針, 或者是一個uint64_t整數, 又或者是一個int64_t整數。
  3. next屬性是指向另一個哈希表節點的指針, 這個指針可以將多個哈希值相同的鍵值對連接在一次, 以此來解決鍵沖突(collision)的問題。
  • 字典
    字典結構用dict表示,此結構能很方便的支持rehash
typedef struct dict {
    dictType *type; /* 類型特定函數 */
    void *privdata; /* 私有數據 */
    dictht ht[2]; /* 哈希表 */
    int rehashidx; /* rehash 索引 當 rehash 不在進行時,值為 -1 */
    int iterators; /* 目前正在運行的安全迭代器的數量 */
} dict;
  1. type屬性和privdata屬性是針對不同類型的鍵值對, 為創建多態字典而設置的
  2. ht屬性是一個包含兩個項的數組, 數組中的每個項都是一個dictht哈希表, 一般情況下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只會在對 ht[0] 哈希表進行 rehash 時使用
  3. 除了 ht[1] 之外, 另一個和 rehash 有關的屬性就是rehashidx: 它記錄了 rehash 目前的進度, 如果目前沒有在進行 rehash , 那么它的值為 -1 。

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


普通字典結構.png

哈希算法

當要將一個新的鍵值對添加到字典里面時, 程序需要先根據鍵值對的鍵計算出哈希值和索引值, 然后再根據索引值, 將包含新鍵值對的哈希表節點放到哈希表數組的指定索引上面。
Redis 計算哈希值和索引值的方法如下:

// 使用字典設置的哈希函數,計算鍵 key 的哈希值
hash = dict->type->hashFunction(key);

// 使用哈希表的 sizemask 屬性和哈希值,計算出索引值
// 根據情況不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

可以看出,redis對進入的字符串首先做一層hashFunction操作,將其轉換成unsigned int型, 再將其和hash表的sizemask做與操作,得到index。

圖4-4.png

舉個例子, 對于圖 4-4 所示的字典來說, 如果我們要將一個鍵值對 k0 和 v0 添加到字典里面, 那么程序會先使用語句:
hash = dict->type->hashFunction(k0);
計算鍵 k0 的哈希值。
假設計算得出的哈希值為 8 , 那么程序會繼續使用語句:
index = hash & dict->ht[0].sizemask = 8 & 3 = 0;
計算出鍵 k0 的索引值 0 , 這表示包含鍵值對 k0 和 v0 的節點應該被放置到哈希表數組的索引 0 位置上, 如圖 4-5 所示。
圖4-5.png

當字典被用作數據庫的底層實現, 或者哈希鍵的底層實現時, Redis 使用 MurmurHash2 算法來計算鍵的哈希值。
可參考:http://code.google.com/p/smhasher/

解決沖突

當有兩個或以上數量的鍵被分配到了哈希表數組的同一個索引上面時, 我們稱這些鍵發生了沖突(collision)。

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

此處簡單,不再摘出書中例子。

rehash

隨著操作的不斷執行, 哈希表保存的鍵值對會逐漸地增多或者減少, 為了讓哈希表的負載因子(load factor)維持在一個合理的范圍之內, 當哈希表保存的鍵值對數量太多或者太少時, 程序需要對哈希表的大小進行相應的擴展或者收縮。

擴展和收縮哈希表的工作可以通過執行 rehash (重新散列)操作來完成, Redis 對字典的哈希表執行 rehash 的步驟如下:

  1. 為字典的 ht[1] 哈希表分配空間, 這個哈希表的空間大小取決于要執行的操作, 以及 ht[0] 當前包含的鍵值對數量 (也即是 ht[0].used 屬性的值):
    • 如果執行的是擴展操作, 那么 ht[1] 的大小為第一個大于等于 ht[0].used * 2 的 2^n (2 的 n 次方冪);
    • 如果執行的是收縮操作, 那么 ht[1] 的大小為第一個大于等于 ht[0].used 的 2^n 。
  2. 將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 上面: rehash 指的是重新計算鍵的哈希值和索引值, 然后將鍵值對放置到 ht[1] 哈希表的指定位置上。
  3. 當 ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之后 (ht[0] 變為空表), 釋放 ht[0] , 將 ht[1] 設置為 ht[0] , 并在 ht[1] 新創建一個空白哈希表, 為下一次 rehash 做準備。

舉個例子, 假設程序要對圖 4-8 所示字典的 ht[0] 進行擴展操作, 那么程序將執行以下步驟:

  1. ht[0].used 當前的值為 4 , 4 * 2 = 8 , 而 8 (2^3)恰好是第一個大于等于 4 的 2 的 n 次方, 所以程序會將 ht[1] 哈希表的大小設置為 8 。 圖 4-9 展示了 ht[1] 在分配空間之后, 字典的樣子。
  2. 將 ht[0] 包含的四個鍵值對都 rehash 到 ht[1] , 如圖 4-10 所示。
  3. 釋放 ht[0] ,并將 ht[1] 設置為 ht[0] ,然后為 ht[1] 分配一個空白哈希表,如圖 4-11 所示。

至此, 對哈希表的擴展操作執行完畢, 程序成功將哈希表的大小從原來的 4 改為了現在的 8 。
圖 4-12 至圖 4-17 展示了一次完整的漸進式 rehash 過程, 注意觀察在整個 rehash 過程中, 字典的 rehashidx 屬性是如何變化的。


4-12.png

4-13.png

4-14.png

4-15.png

4-16.png

4-17.png

字典API

函數 作用 時間復雜度
dictCreate 創建一個新的字典。 O(1)
dictAdd 將給定的鍵值對添加到字典里面。 O(1)
dictReplace 將給定的鍵值對添加到字典里面, 如果鍵已經存在于字典,那么用新值取代原有的值。 O(1)
dictFetchValue 返回給定鍵的值。 O(1)
dictGetRandomKey 從字典中隨機返回一個鍵值對。 O(1)
dictDelete 從字典中刪除給定鍵所對應的鍵值對。 O(1)
dictRelease 釋放給定字典,以及字典中包含的所有鍵值對。 O(N) , N 為字典包含的鍵值對數量。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容