1. 總體結構
redis的dict就是hash表,使用鏈式結構來解決key值衝突,典型的資料結構
結構體的定義如下:
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; //hash桶是一個指標陣列,裡面存放的是hash entry的指標型別,只需要(8位元組*size)個連續記憶體不需要大量的連續記憶體
unsigned long size; //這個是hash桶的大小
unsigned long sizemask; //hash桶大小-1, **用hash**/sizemask來計算桶下標
unsigned long used; //當前這個dict一共放了多少個kv鍵值對
} dictht;
//一旦used/size >=dict_force_resize_ratio(預設值是5),就會觸發rehash,可以理解為一個hash桶後面平均掛載的衝突佇列個數為5的時候,就會觸發rehash
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;
複製程式碼
如下圖所示:
2. API介面分析
2.1 建立
API介面函式:
- dictAdd(dict *d, void *key, void *val)
在d中增加一個k-v對,實現程式碼如下:
/* Add an element to the target hash table */
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);//呼叫了內部函式
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
dictht *ht;
if (dictIsRehashing(d)) _dictRehashStep(d); //如果正在rehash進行中,則每次操作都嘗試進行一次rehash操作
/* Get the index of the new element, or -1 if
* the element already exists. 獲取到hash桶的入口index*/
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
/* Allocate the memory and store the new entry.
* Insert the element in top, with the assumption that in a database
* system it is more likely that recently added entries are accessed
* more frequently.
(譯文:申請記憶體來儲存一個新的entry結構,插入元素到頭部,
這裡的實現和一般的hash鏈式解決衝突的實現有點小不同,基於這樣的假定:在資料庫系統中,最近增加的entries越有可能被訪問。
這裡是把新插入的entry放到了連結串列頭上,可以看上面的英文解釋*/
ht = dictIsRehashing(d) ? &d->ht[1] : &d->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;
}
/* Returns the index of a free slot that can be populated with
* a hash entry for the given 'key'.
* If the key already exists, -1 is returned
* and the optional output parameter may be filled.
*
* Note that if we are in the process of rehashing the hash table, the
* index is always returned in the context of the second (new) hash table.
這個原版註釋寫的很清楚,如果正在rehashing的時候,index返回的是new的hashtable*/
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;
/* Expand the hash table if needed ,判斷hash桶是否需要擴大,這個地方是redis比較牛逼的地方,
hash桶是動態擴大的,預設初始的時候只有4,然後每次乘2的方式進行擴充套件,如果擴充套件了,就需要進行rehash*/
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
/*獲取索引的時候,如果正在rehash,需要兩個hashtable都進行查詢*/
for (table = 0; table <= 1; table++) {
/*這個idx就是hash桶的下標*/
idx = hash & d->ht[table].sizemask;
/* Search if this slot does not already contain the given key */
he = d->ht[table].table[idx];
while(he) {
/*這裡是必須遍歷下衝突佇列,保證key沒有出現過*/
if (key==he->key || dictCompareKeys(d, key, he->key)) {
if (existing) *existing = he;
return -1;
}
he = he->next;
}
/*如果不在rehash的話,其實就沒有必要再做查詢的操作了,直接返回就好了*/
if (!dictIsRehashing(d)) break;
}
return idx;
}
複製程式碼
- dictEntry *dictFind(dict *d, const void *key) 根據key在d中尋找值,這個邏輯和add差不多,程式碼很簡單,這裡就不做解釋了
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
if (dictIsRehashing(d)) _dictRehashStep(d); //和增加的時候邏輯一樣,如果正在rehashing,則進行一步rehash
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
複製程式碼
3. rehash過程
redis對於dict支援兩種rehash的方式:按照時間,或者按照操作進行rehash。每次都調整一個key值桶內所有的衝突連結串列到新的hash表中。
rehash 程式碼如下:
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
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;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
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; //redis為了保證效能,掃描空桶,最多也是有一定的限制
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT ,這個迴圈就是開始把這個rehashidx下標的hashtable遷移到新的下標下面,注意,這裡需要重新計算key值,重新插入*/
while(de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;//重新計算key值,重新插入
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++;
}
/* Check if we already rehashed the whole table...,一次操作完了,可能這個hashtable已經遷移完畢,返回0,否則返回1 */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1]; //現在的0變成1
_dictReset(&d->ht[1]); //現在的1被reset掉
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
複製程式碼