dict是一種用於保存鍵值對的抽象數據結構,在redis中使用非常廣泛,比如資料庫、哈希結構的底層。 ...
dict的用途
dict是一種用於保存鍵值對的抽象數據結構,在redis中使用非常廣泛,比如資料庫、哈希結構的底層。
當執行下麵這個命令:
> set msg "hello"
以及使用哈希結構,如:
> hset people name "hoohack"
都會使用到dict作為底層數據結構的實現。
結構的定義
先看看字典以及相關數據結構體的定義:
字典
/* 字典結構 每個字典有兩個哈希表,實現漸進式哈希時需要用在將舊表rehash到新表 */
typedef struct dict {
dictType *type; /* 類型特定函數 */
void *privdata; /* 保存類型特定函數需要使用的參數 */
dictht ht[2]; /* 保存的兩個哈希表,ht[0]是真正使用的,ht[1]會在rehash時使用 */
long rehashidx; /* rehashing not in progress if rehashidx == -1 rehash進度,如果不等於-1,說明還在進行rehash */
unsigned long iterators; /* number of iterators currently running 正在運行中的遍歷器數量 */
} dict;
哈希表
/* 哈希表結構 */
typedef struct dictht {
dictEntry **table; /* 哈希表節點數組 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 哈希表大小掩碼,用於計算哈希表的索引值,大小總是dictht.size - 1 */
unsigned long used; /* 哈希表已經使用的節點數量 */
} dictht;
哈希表節點
/* 哈希表節點 */
typedef struct dictEntry {
void *key; /* 鍵名 */
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; /* 值 */
struct dictEntry *next; /* 指向下一個節點, 將多個哈希值相同的鍵值對連接起來*/
} dictEntry;
dictType
/* 保存一連串操作特定類型鍵值對的函數 */
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;
把上面的結構定義串起來,得到下麵的字典數據結構:
根據數據結構定義,把關聯圖畫出來後,看代碼的時候就更加清晰。
從圖中也可以看出來,字典的哈希表裡,使用了鏈表解決鍵衝突的情況,稱為鏈式地址法。
rehash(重新散列)
當操作越來越多,比如不斷的向哈希表添加元素,此時哈希表需要分配了更多的空間,如果接下來的操作是不斷地刪除哈希表的元素,那麼哈希表的大小就會發生變化,更重要的是,現在的哈希表不再需要那麼大的空間了,在redis的實現中,為了保證哈希表的負載因數維持在一個合理範圍內,當哈希表保存的鍵值對太多或者太少時,redis對哈希表大小進行相應的擴展和收縮,稱為rehash(重新散列)。
執行rehash的流程圖
負載因數解釋
負載因數 = 哈希表已保存節點數量 / 哈希表大小
負載因數越大,意味著哈希表越滿,越容易導致衝突,性能也就越低。因此,一般來說,當負載因數大於某個常數(可能是 1,或者 0.75 等)時,哈希表將自動擴容。
漸進式rehash
在上面的rehash流程圖裡面,rehash的操作不是一次性就完成了的,而是分多次,漸進式地完成。
原因是,如果需要rehash的鍵值對較多,會對伺服器造成性能影響,漸進式地rehash避免了對伺服器的影響。
漸進式的rehash使用了dict結構體中的rehashidx屬性輔助完成。當漸進式哈希開始時,rehashidx會被設置為0,表示從dictEntry[0]開始進行rehash,每完成一次,就將rehashidx加1。直到ht[0]中的所有節點都被rehash到ht[1],rehashidx被設置為-1,此時表示rehash結束。
結合代碼再深入理解
/* 實現漸進式的重新哈希,如果還有需要重新哈希的key,返回1,否則返回0
*
* 需要註意的是,rehash持續將bucket從老的哈希表移到新的哈希表,但是,因為有的哈希表是空的,
* 因此函數不能保證即使一個bucket也會被rehash,因為函數最多一共會訪問N*10個空bucket,不然的話,函數將會耗費過多性能,而且函數會被阻塞一段時間
*/
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
assert(d->ht[0].size > (unsigned long)d->rehashidx);
/* 找到非空的哈希表下標 */
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* 實現將bucket從老的哈希表移到新的哈希表 */
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* 如果已經完成了,釋放舊的哈希表,返回0 */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* 繼續下一次rehash */
return 1;
}
在漸進式rehash期間,所有對字典的操作,包括:添加、查找、更新等等,程式除了執行指定的操作之外,還會順帶將ht[0]哈希表索引的所有鍵值對rehash到ht[1]。比如添加:
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
int index;
dictEntry *entry;
dictht *ht;
/* 如果正在rehash,順帶執行rehash操作 */
if (dictIsRehashing(d)) _dictRehashStep(d);
/* 獲取新元素的下標,如果已經存在,返回-1 */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 如果正在進行rehash操作,返回ht[1],否則返回ht[0]
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
總結
使用一個標記值標記某項操作正在執行是編程中常用的手段,比如本文提到的rehashidx,多利用此手段可以解決很多問題。
我在github有對Redis源碼更詳細的註解。感興趣的可以圍觀一下,給個star。Redis4.0源碼註解。可以通過commit記錄查看已添加的註解。
原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
更多精彩內容,請關註個人公眾號。