typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
uint64_t (*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;
/* 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;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
字典dict主要內容在於2個dictht表和rehashidx,dictht[0]表示沒有進行哈希的表,dict[1]表示正在進行哈希的表,rehashidx不爲-1的時候表示正在進行哈希。dictht主要內容sizemask始終是size減1。
int dictExpand(dict *d, unsigned long size)
用來擴大字典大小,字典大小永遠爲2的倍數,方便查找。如果0號表沒有初始化過,那麼直接給0號表賦值,如果0號表存在的話,就擴大的表賦值給1號表,準備哈希。
int dictRehash(dict *d, int n)
對字典進行哈希,參數n表示要執行次數。由於字典會有空值,所以設置空值瀏覽次數,這個值設置爲執行次數的10倍。每次執行哈希的過程如下:首先從0號表中找到一個非空實體,把這個實體鏈表裏的內容全部遷移到1號表對應下標的表裏,往鏈表首部賦值。如果哈希完成則把1號表的內容全部遷移回0號表,清空1號表並返回0,否則返回1。
下圖引用自https://redissrc.readthedocs.io/en/latest/datastruct/dict.html,把字典中添加新元素的過程描述的很清楚了。
/* Function to reverse bits. Algorithm from:
* http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel */
static unsigned long rev(unsigned long v) {
unsigned long s = 8 * sizeof(v); // bit size; must be power of 2
unsigned long mask = ~0;
while ((s >>= 1) > 0) {
mask ^= (mask << s);
v = ((v >> s) & mask) | ((v << s) & ~mask);
}
return v;
}
這個對一個數進行高低位相反的操作的算法蠻有意思的,網站上講解得很詳細。
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction* bucketfn, void *privdata)
字典的數據結構和算法裏,dictScan這個函數的算法特別精妙。遍歷的步驟如下:第一步調用函數,遊標(v)設置爲0;第二步執行一次遍歷,會返回一個新的遊標,供下一次調用使用;第三步當遊標返回爲0的時候整個遍歷就結束了。
由於字典在遍歷的時候大小可能會擴大或者縮小,所以普通的遍歷算法難免會過多或過少地遍歷字典,雖然這個算法也會多遍歷幾次字典,但是它保證了一定會遍歷所有元素。
字典的大小一定是2的n次方,字典的掩碼是n位全1的數,所以取元素的時候是根據哈希值並掩碼取得,保證遊標一定在字典分配的大小範圍內。遍歷的時候字典的大小沒有發生變化的這種情況沒有什麼好說的,關鍵在於字典的大小發生變化的時候。
假設字典的大小是16,掩碼就是0b1111,以字典遍歷到0b1100字典大小發生變化爲例。
當字典的大小變大到32時,當前遊標就變成了0b01100。字典遍歷過的遊標由0b0000,0b1000,0b0100變爲0b00000,0b10000,0b01000,0b11000,0b00100,0b10100,也就是說之前遍歷過的遊標不會再遍歷到。
當字典的大小變小到8時,當前遊標就變成了0b100。字典遍歷過的遊標由0b0000,0b1000,0b0100變爲0b000,0b100,這時候會重新遍歷一次0b100這個遊標,但是也保證了所有元素會被遍歷到。