redis源碼之字典dict

未完待續…

字典dict

1.簡介:

它支持插入、刪除、替換、查找和獲取隨機元素等操作。
哈希表會自動在表的大小的二次方之間進行調整。
鍵的衝突通過鏈表來解決。
rehash

2.定義

/*
 * 1.哈希表節點
 */
typedef struct dictEntry {

    // 鍵
    void *key;

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

    // 指向下個哈希表節點,形成鏈表
    struct dictEntry *next;

} dictEntry;

/*
 * 2.哈希表
 * 每個字典都使用兩個哈希表,從而實現漸進式 rehash 。
 */
typedef struct dictht {

    // 哈希表數組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩碼,用於計算索引值
    // 總是等於 size - 1
    unsigned long sizemask;

    // 該哈希表已有節點的數量
    unsigned long used;

} dictht;

/*
 * 3.字典
 */
typedef struct dict {

    // 類型特定函數
    dictType *type;

    // 私有數據,保存了需要傳給那些類型特定畫數的可選參數。
    void *privdata;

    // 哈希表,一般情況下,字典只使用ht[OJ 晴希表, ht[1]晗希表只會在對ht[0]哈希表進行rehash 時使用。
    dictht ht[2];

    // rehash 索引
    // 當 rehash 不在進行時,值爲 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在運行的安全迭代器的數量
    int iterators; /* number of iterators currently running */

} dict;

/*
 * 4.字典類型特定函數
 */
typedef struct dictType {

    // 計算哈希值的函數
    unsigned int (*hashFunction)(const void *key);

    // 複製鍵的函數
    void *(*keyDup)(void *privdata, const void *key);

    // 複製值的函數
    void *(*valDup)(void *privdata, const void *obj);

    // 對比鍵的函數
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);

    // 銷燬鍵的函數
    void (*keyDestructor)(void *privdata, void *key);

    // 銷燬值的函數
    void (*valDestructor)(void *privdata, void *obj);

} dictType;

這裏寫圖片描述
這裏寫圖片描述

3.hash算法,得到索引值

Murmurhash2和DJBhash
Redis 計算晗希值和索引值的方法如下:
#使用字典設置的哈希函數,計算鍵key 的哈希值
hash= dict->type->hashFunction(key);
#使用哈希衰的sizemask 屬性和哈希值,計算出索引值
#根據情況不同, ht[x]可以是ht[0l 或者ht[1]
index= hash 品dict->ht[x].sizemask;

解決鍵衝突
鏈地址法(因爲 dictEntry 節點組成的鏈表沒有指向鏈表表尾的指針, 所以爲了速度考慮,程序總是將新節點添加到鏈表的表頭位置(複雜度爲O(1)), 排在其他已有節點的前面。 )

4.rehash

爲了讓晗希表的負載因子( load factor )維持在一個合理的範圍之內,當哈希表保存的鍵值對數量太多或者太少時,程序需要對晗希表的大小進行相應的擴展或者收縮。
(1)爲字典的ht[1]哈希表分配空間:
(1)如果執行的是擴展操作,那麼ht[1]的大小爲第一個大於等於ht[0] .used*2
的2”( 2 的n 次方幕);
(2)如果執行的是收縮操作,那麼ht[1]的大小爲第一個大於等於ht[0] .used的2”;、
(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
做準備。
哈希表的擴展與收縮條件
負載因子 = 哈希表已保存節點數量 / 哈希表大小,load factor= ht[O].used I ht[OJ.size
負載因子大於1是因爲,size是table的數量,而每個table還有鏈表呢 !
當以下條件中的任意一個被滿足時,程序會自動開始對晗希表執行擴展操作:
(1)服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於於 1 ;
(2)服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於等於 5 ;
當晗希表的負載因子小於0.1 時,程序自動開始對晗希表執行收縮操作。
在執行BGSA陽命令或BGREWRITEAOF 命令的過程中, Redis 需要創建當前服務器進程的子進程,而大多數操作系統都採用寫時複製( copy-onwrite)技術來優化子進程的使用效率,所以在子進程存在期間,服務器會提高執行擴展操作所需的負載因子,從而儘可能地避免在子進程存在期間進行晗希表擴展操作,這可以避免不必要的內存寫入操作,最大限度地節約內存。

5.漸進式 rehash:索引0->sizemask

rehash 動作並不是一次性、集中式地完成的,而是分多次、漸進式地完成的。原因在於,一次性將這些鍵值對全部rehash到ht[1]的話,龐大的計算量可能會導致服務器在一段時間內停止服務。
以下是哈希表漸進式 rehash 的詳細步驟:
(1)爲 ht[1] 分配空間, 讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。
(2)在字典中維持一個索引計數器變量 rehashidx , 並將它的值設置爲 0 , 表示 rehash 工作正式開始。
(3)在 rehash 進行期間, 每次對字典執行添加、刪除、查找或者更新操作時, 程序除了執行指定的操作以外, 還會順帶將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1] , 當 rehash 工作完成之後, 程序將 rehashidx 屬性的值增1。
(4)隨着字典操作的不斷執行, 最終在某個時間點上, ht[0] 的所有鍵值對都會被 rehash 至 ht[1] , 這時程序將 rehashidx 屬性的值設爲 -1 , 表示 rehash 操作已完成。

1.因爲在進行漸進式 rehash 的過程中, 字典會同時使用 ht[0] 和 ht[1] 兩個哈希表, 所以在漸進式 rehash 進行期間, 字典的刪除(delete)、查找(find)、更新(update)等操作會在兩個哈希表上進行: 比如說, 要在字典裏面查找一個鍵的話, 程序會先在ht[0] 裏面進行查找, 如果沒找到的話, 就會繼續到 ht[1] 裏面進行查找, 諸如此類。
2.另外, 在漸進式 rehash 執行期間, 新添加到字典的鍵值對一律會被保存到 ht[1] 裏面, 而 ht[0] 則不再進行任何添加操作: 這一措施保證了 ht[0] 包含的鍵值對數量會只減不增, 並隨着 rehash 操作的執行而最終變成空表。

typedef struct dictIterator {
    // 被迭代的字典
    dict *d;
    // table :正在被迭代的哈希表號碼,值可以是 0 或 1 。
    // index :迭代器當前所指向的哈希表索引位置。
    // safe :標識這個迭代器是否安全
    int table, index, safe;
    // entry :當前迭代到的節點的指針
    // nextEntry :當前迭代節點的下一個節點
    // 因爲在安全迭代器運作時, entry 所指向的節點可能會被修改,
    // 所以需要一個額外的指針來保存下一節點的位置,
    // 從而防止指針丟失
    dictEntry *entry, *nextEntry;
    long long fingerprint; // unsafe iterator fingerprint for misuse detection
} dictIterator;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章