Redis源碼:dict數據結構(聲明)

Redis的dict,譯爲“字典”,猶如Python裏的字典,底層是用哈希表實現的,好的實現可以達到O(1)的時間複雜度。衆所周知,哈希表其實就是一個數組,一個元素插入到哈希表時,先通過一個哈希函數將鍵值映射到一個下標。存儲和讀取都是如此。(ps,如果對哈希表這個數據結構一無所知,最好先自己學習完、實現一遍,再來看Redis的源碼)

由於可能會有多個元素映射到同一個下標,即會產生“衝突問題”,那就需要選擇一種方案來解決,總的有兩種方案,一種是開放尋址法,也就是在同一個位置做一個鏈表;一種是重新在表裏找一個位置來放置它,比如線性探查法、平方探查法、雙散列法、再散列法。

哈希表的元素:dictEntry

如上所說,哈希表就是一個數組,那麼問題來了,這個數組裏的元素的類型是什麼?

值得注意的是,一個元素一般是(鍵: 值)的形式,比如(“jacket”: 666)。我們要能容納各種類型的元素,無論是鍵的類型,還是值的類型。這在Python這種弱類型語言裏寫起來很方便,但是Redis是用C語言寫的,要支持任意類型,該怎麼實現呢?

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

哈哈,答案是void*
dictEntry這個結構體只有三個數據成員,key很直觀。

value成員(代碼裏簡寫爲v)是一個union,如果不清楚union是什麼的話,請自行搜索,這裏簡單介紹一下。它相當於全部成員裏的值都是一樣的,只有一份,但是這個值有多種解釋。具體一點,別看v有“四個數據成員”就認爲它所佔用的字節數是8 byte * 4 = 32 byte,這裏的v所佔的字節數其實只有一份,即8 byte。假設v = 0x 0000 0000 0000 0001,按照上面的定義,可以解釋爲一個任意類型的指針值,或者說它是一個無符號的64位整數,或者說它是一個有符號的64位整數,又或者說它是一個double類型的浮點數。如果你到現在還無法理解“一份值,有多種解釋”的話,那你讀多幾遍上面這個分析吧,或者先google一圈“C語言的union”再繼續往下讀。

最後是next成員,它的類型是struct dictEntry *,說明它是一個單鏈表的結點,也就是它解決哈希表衝突的方法是開放尋址法。

哈希表的結構

先請你再讀一遍這句話:“dict底層是用哈希表實現的”,也就是說,我們看到dict的真面目之前,還需要先通過哈希表這一關,先看代碼:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictht就是dict hash table的簡寫,往下看,哎,是不是很納悶呢,怎麼一個哈希表需要這麼多元素啊,不就一個數組(dictEntry **table),再加一個大小(size)就可以了嗎?怎麼還有後面兩個成員sizemask和used?

首先是used,這個意思很清晰,就是已經插入了多少元素,爲了計算負載因子(load factor,設元素數量爲k,哈希表大小爲n,那麼load factor=k/n),至於爲什麼要計算這個值,很快會談到。

然後sizemask是用來幹嘛的?這個涉及到哈希表的具體實現,一般是這樣的,哈希函數將鍵值映射到一個很大的範圍,比如[0, 999999999999997],然後哈希表的大小比如只有100,那麼大於等於100的數字該怎麼辦呢?取個模唄,比如233 % 100 = 33。但是我們知道,取模運算的效率並不是跟加減法一樣快速的,它相對來說是一個很慢的操作!那怎麼辦呢?

嗯哼,這當然難不倒聰明的程序員們!可以把哈希表的實際大小放大到128(也就是大於等於100的最小的2的整數次冪),然後給定一個二進制的mask,0000 0000 0111 1111,將數值與mask位與一下,就可以實現跟“取模”一樣的效果了,而位與操作相對來說是超級快速的。

注意到上面的註釋裏提到了“incremental rehashing”,什麼是“增量重哈希”(直譯的名字,別介意)呢?查詢維基百科可以知道:
incremental rehashing

上面的英文都很簡單,就不翻譯了。意思就是,當哈希表的負載因子達到一定程度時,繼續插入元素很容易產生很多衝突,從而影響總體的性能(如上所述,Redis用了拉鍊法,衝突增多會導致鏈表越來越長,從而使得平均查找時間也會越來越長),所以需要新建一個更大的哈希表來降低衝突的頻率。那麼問題來了,新建一個哈希表,如果直接一次性把所有的元素複製過去,是很耗時的,對於實時性要求較高的應用來說根本不能忍!那怎麼辦呢?

不要一次性複製咯,每次插入一個元素到新的表裏時,順便從舊的表裏複製r個元素過去(這個r是自己定的一個常數,不能太大,也不要太小)。具體的細節等到看實現的代碼再來了解!

現在再回頭看看爲什麼哈希表裏面的數組聲明爲dictEntry **table就很清晰了,因爲這個數組的大小是動態變化的,需要是一個動態數組。那爲什麼元素類型是dictEntry *而不是單純的dictEntry呢?

我猜,這是爲了在複製的時候節省時間。從上面dictEntry的聲明知道,它的大小是24個字節(在64位機器上),而複製一個dictEntry *只需要複製8個字節,突然間省了2/3的時間,是不是很開心?複製發生在插入的時候,還有上面講到的“incremental rehashing”,涉及到複製整張舊的表。所以dictEntry *是很用心的一個設計細節,給作者贊一個!

dict來了

終於見到了dict的廬山真面目!

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

嘿?dictType是什麼鬼?這個在前面跳過了,它的定義如下:

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;

可以看出,它就是一堆函數指針的集合,具體幹嘛的,從函數的命名上可以看出個大概,不過暫且不管它,等下一篇分析實現的時候再回頭來看!

h接着是void *privdata,就是private data的縮寫,私有數據?用來幹嘛的?先不告訴你……

再接着是dictht ht[2],這個前面已經談到了,增量重哈希,所以就需要兩個哈系表。

rehashidx這個也是跟增量重哈希有關的,因爲我們每次是隻複製r個元素到新的表中,所以需要記錄當前已經複製到哪個位置了。當其值爲-1的時候表示當前狀態沒有進行增量重哈希;當其值爲非負整數時就表示當前正在進行增量重哈希,並且下一次從rehashidx這裏開始複製r個元素到新的表中。

最後是iterators,這個暫時不懂,等實現篇再來詳細介紹。ps,跟這個值相關的,還有以下這個聲明:

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;

typedef void (dictScanFunction)(void *privdata, const dictEntry *de);

各種define

下面這個是哈希表的初始大小……(廢話)

/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE     4

然後是這個:

#define dictFreeVal(d, entry) \
    if ((d)->type->valDestructor) \
        (d)->type->valDestructor((d)->privdata, (entry)->v.val)

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        entry->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        entry->v.val = (_val_); \
} while(0)

使用宏,對閱讀源代碼一個很不好的地方就是,你不知道參數是什麼類型的……不過自己看看使用它們的代碼也能知道,這裏的d表示dict,也就是一個字典,entry是一個元素,_val_表示的是void*的值,類型對應dictEntry.v。

(d)->type->valDestructor就是上面談到的dictType這個結構體裏的東西,我認爲它就是一堆回調函數,有點像在模擬C++裏面的類成員函數。上面代碼的意思是,如果某個函數指針不爲空,就調用它,否則就……問題是爲什麼用do … while(0)呢?雖然這個東西的效果跟調用一次是一樣的(因爲0代表false,所以while(0)就直接退出了)。這個涉及到define的一個深坑,有興趣可以看這個博客的介紹:do {…} while (0) 在宏定義中的作用,大意就是,這樣子寫更好更安全!

接下來這幾個很容易理解啦,就是以多種方式設置dictEntry.v的值:

#define dictSetSignedIntegerVal(entry, _val_) \
    do { entry->v.s64 = _val_; } while(0)

#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { entry->v.u64 = _val_; } while(0)

#define dictSetDoubleVal(entry, _val_) \
    do { entry->v.d = _val_; } while(0)

再往下都是跟上面差不多的定義,我覺得如果連這些都看不懂……那得好好練習基本功而不是繼續看代碼了(中性的勸誡):

#define dictFreeKey(d, entry) \
    if ((d)->type->keyDestructor) \
        (d)->type->keyDestructor((d)->privdata, (entry)->key)

#define dictSetKey(d, entry, _key_) do { \
    if ((d)->type->keyDup) \
        entry->key = (d)->type->keyDup((d)->privdata, _key_); \
    else \
        entry->key = (_key_); \
} while(0)

#define dictCompareKeys(d, key1, key2) \
    (((d)->type->keyCompare) ? \
        (d)->type->keyCompare((d)->privdata, key1, key2) : \
        (key1) == (key2))

#define dictHashKey(d, key) (d)->type->hashFunction(key)
#define dictGetKey(he) ((he)->key)
#define dictGetVal(he) ((he)->v.val)
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
#define dictGetDoubleVal(he) ((he)->v.d)
#define dictSlots(d) ((d)->ht[0].size+(d)->ht[1].size)
#define dictSize(d) ((d)->ht[0].used+(d)->ht[1].used)
#define dictIsRehashing(d) ((d)->rehashidx != -1)

API

最後這些先略過,在下一篇實現篇裏介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章