Redis原始碼分析(dict)
一、dict 簡介
dict
(dictionary 字典),通常的儲存結構是Key-Value
形式的,通過Hash函式
對key求Hash值來確定Value的位置,因此也叫Hash表,是一種用來解決演算法中查詢問題
的資料結構,預設的演算法複雜度接近O(1),Redis本身也叫REmote
DIctionary Server (遠端字典伺服器)
,其實也就是一個大字典,它的key
通常來說是String
型別的,但是Value
可以是 String、Set、ZSet、Hash、List
等不同的型別,下面我們看下dict的資料結構定義。
二、資料結構定義
與dict相關的關鍵資料結構有三個,分別是:
dictEntry
表示一個Key-Value節點。dictht
表示一個Hash表。dict
是Redis中的字典結構,包含兩個dictht
。
dictEntry結構的程式碼如下:
typedef struct dictEntry {
void *key; //key void*表示任意型別指標
union { //聯合體中對於數字型別提供了專門的型別優化
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //next指標
} dictEntry;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
dictht的程式碼如下:
typedef struct dictht {
dictEntry **table; //陣列指標,每個元素都是一個指向dictEntry的指標
unsigned long size; //表示這個dictht已經分配空間的大小,大小總是2^n
unsigned long sizemask; //sizemask = size - 1; 是用來求hash值的掩碼,為2^n-1
unsigned long used; //目前已有的元素數量
} dictht;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
最後是真正的dict結構:
typedef struct dict {
dictType *type; //type中定義了對於Hash表的操作函式,比如Hash函式,key比較函式等
void *privdata; //privdata是可以傳遞給dict的私有資料
dictht ht[2]; //每一個dict都包含兩個dictht,一個用於rehash
int rehashidx; //表示此時是否在進行rehash操作
int iterators; //迭代器
} dict;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
其實通過上面的三個資料結構,已經可以大概看出dict的組成,資料(Key-Value)儲存在每一個dictEntry節點;然後一條Hash表就是一個dictht結構,裡面標明瞭Hash表的size,used等資訊;最後每一個Redis的dict結構都會預設包含兩個dictht,如果有一個Hash表滿足特定條件需要擴容,則會申請另一個Hash表,然後把元素ReHash過來,ReHash的意思就是重新計算每個Key的Hash值,然後把它存放在第二個Hash表合適的位置,但是這個操作在Redis中並不是集中式一次完成的,而是在後續的增刪改查
過程中逐步完成的,這個叫漸進式ReHash,我們後文會專門討論。
三、建立、插入、鍵衝突、擴張
下面我們跟隨一個例子來看有關dict的建立,插入,鍵衝突的解決辦法以及擴張的問題。在這裡推薦一個有關除錯Redis資料結構程式碼的方法:下載一份Redis原始碼,然後直接把server.c
中main
函式註釋掉,加入自己的程式碼,直接make
之後就可以跑了。我們的例子如下所示:
int main(int argc, char **argv) {
int ret;
sds key = sdsnew("key");
sds val = sdsnew("val");
dict *dd = dictCreate(&keyptrDictType, NULL);
printf("Add elements to dict\n");
for (int i = 0; i < 6 ; ++i) {
ret = dictAdd(dd, sdscatprintf(key, "%d", i), sdscatprintf(val, "%d", i));
printf("Add ret%d is :%d ,", i, ret);
printf("ht[0].used :%lu, ht[0].size :%lu, "
"ht[1].used :%lu, ht[1].size :%lu\n", dd->ht[0].used, dd->ht[0].size, dd->ht[1].used, dd->ht[1].size);
}
printf("\nDel elements to dict\n");
for (int i = 0; i < 6 ; ++i) {
ret = dictDelete(dd, sdscatprintf(key, "%d", i));
printf("Del ret%d is :%d ,", i, ret);
printf("ht[0].used :%lu, ht[0].size :%lu, "
"ht[1].used :%lu, ht[1].size :%lu\n", dd->ht[0].used, dd->ht[0].size, dd->ht[1].used, dd->ht[1].size);
}
sdsfree(key);
sdsfree(val);
dictRelease(dd);
return 0;
}
Out >
Add elements to dict
Add ret0 is :0 ,ht[0].used :1, ht[0].size :4, ht[1].used :0, ht[1].size :0
Add ret1 is :0 ,ht[0].used :2, ht[0].size :4, ht[1].used :0, ht[1].size :0
Add ret2 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :0, ht[1].size :0
Add ret3 is :0 ,ht[0].used :4, ht[0].size :4, ht[1].used :0, ht[1].size :0
Add ret4 is :0 ,ht[0].used :4, ht[0].size :4, ht[1].used :1, ht[1].size :8
Add ret5 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :3, ht[1].size :8
Del elements to dict
Del ret0 is :0 ,ht[0].used :5, ht[0].size :8, ht[1].used :0, ht[1].size :0
Del ret1 is :0 ,ht[0].used :4, ht[0].size :8, ht[1].used :0, ht[1].size :0
Del ret2 is :0 ,ht[0].used :3, ht[0].size :8, ht[1].used :0, ht[1].size :0
Del ret3 is :0 ,ht[0].used :2, ht[0].size :8, ht[1].used :0, ht[1].size :0
Del ret4 is :0 ,ht[0].used :1, ht[0].size :8, ht[1].used :0, ht[1].size :0
Del ret5 is :0 ,ht[0].used :0, ht[0].size :8, ht[1].used :0, ht[1].size :0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
dict *dd = dictCreate(&keyptrDictType, NULL);
建立了一個名為dd,type為keyptrDictType的dict,建立程式碼如下,需要注意的是這個操作只給dict本身申請了空間,但是像dict->ht->table這些資料儲存節點並沒有分配空間,這些空間是dictAdd的時候才分配的。
/* Create a new hash table */
dict *dictCreate(dictType *type,
void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d)); //申請空間,sizeof(*d)為88個位元組
_dictInit(d,type,privDataPtr); //一些置NULL操作,type和privdata置為引數指定值
return d;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
ret = dictAdd(dd, sdscatprintf(key, "%d", i), sdscatprintf(val, "%d", i));
接著我們定義了兩個sds,並且for迴圈分別將他們dictAdd,來看下dictAdd的程式碼,它實際上呼叫了dictAddRaw函式:
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
int index;
dictEntry *entry;
dictht *ht;
if (dictIsRehashing(d)) _dictRehashStep(d);
/* Get the index of the new element, or -1 if
* the element already exists. */
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. */
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
可以看到首先檢測是否在進行ReHash(我們先跳過ReHash這個概念),接下來算出了一個index值,然後根據是否在進行ReHash選擇了其中一個dt(0或者1),之後進行了頭插,而且英文註釋中也寫的很清楚將資料插在頭部基於資料庫系統總是會經常訪問最近新增的節點
,然後將key設定之後就返回了,但是我們貌似還是沒有發現申請空間的函式,其實是在算index的時候_dictKeyIndex()
會自動判斷,如下:
static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing)
{
unsigned int idx, table;
dictEntry *he;
if (existing) *existing = NULL;
/* Expand the hash table if needed */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
for (table = 0; table <= 1; table++) {
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) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
if (existing) *existing = he;
return -1;
}
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return idx;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
_dictExpandIfNeeded(d)
進行空間判斷,如果還未申請,就建立預設大小,其中它裡面也有dict擴容的策略(見註釋):
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
//如果正在ReHash,那直接返回OK,其實也表明申請了空間不久。
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
//如果 0 號雜湊表的大小為0,表示還未建立,按照預設大小`DICT_HT_INITIAL_SIZE=4`去建立
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
//如果滿足 0 號雜湊表used>size &&(dict_can_resize為1 或者 used/size > 5) 那就預設擴兩倍大小
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
對於我們的程式碼,走的是if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
這個分支,也就是會去建立一個dictht的table大小為4的dict,如下:
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
需要注意的是_dictNextPower
可以計算出距離size最近,且大於或者等於size的2的次方的值,比如size是4,那距離其最近的值為4(2的平方),size是6,距離其最近的值為8(2的三次方),然後申請空間,之後判斷如果d->ht[0].table
== NULL
也就是我們目前的還未初始化的情況,則初始化 0 號Hash表,之後新增相應的元素,我們程式的輸出如下所示:
Add ret0 is :0 ,ht[0].used :1, ht[0].size :4, ht[1].used :0, ht[1].size :0
- 1
如果圖示目前的Hash表,如下所示:
- 接下來for迴圈繼續新增,當i = 4時,也就是當新增第5個元素時,預設初始化大小為4的Hash表已經不夠用了。此時的used=4,我們看看擴張操作發生了什麼,程式碼從
_dictExpandIfNeeded(d)
說起,此時滿足條件,會執行擴張操作,如下:
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
dictExpand(d, d->ht[0].used*2);
表示重新申請了一個大小為之前2倍的Hash表,即 1 號Hash表。然後將d->rehashidx = 0;
即表明此時開始ReHash操作。
Rehash就是將原始Hash表(0號Hash表)上的Key重新按照Hash函式計算Hash值,存到新的Hash表(1號Hash表)的過程。
這一步執行之後此時Hash表如下所示:
由圖可以看到 0 號Hash表已經滿了,此時我們的新資料被存到了 1 號雜湊表中,接下來我們開始了第6次迴圈,我們繼續看在ReHash的情況下資料是如何存入的,也就是第6次迴圈,即新增key5的過程,繼續呼叫dictAddRaw函式:
if (dictIsRehashing(d)) _dictRehashStep(d);
- 1
此時因為d->rehashidx = 0
,所以會執行漸進式Hash操作,即_dictRehashStep(d):
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1); //如果迭代器是0,ReHash步長為1
}
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;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
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++;
}
/* Check if we already rehashed the whole table... */
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;
}
/* More to rehash... */
return 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
int empty_visits = n*10;
empty_visits表示每次最多跳過10倍步長的空桶(一個桶就是ht->table陣列的一個位置),然後當我們找到一個非空的桶時,就將這個桶中所有的key全都ReHash到 1 號Hash表。最後每次都會判斷是否將所有的key全部ReHash了,如果已經全部完成,就釋放掉ht[0],然後將ht[1]變成ht[0]。
也就是此次dictAdd操作不僅將key5新增進去,還將 0 號Hash表中2號桶中的key0 ReHash到了 1 號Hash表上。所以此時的 2 號Hash表上有3個元素,如下:
Add ret5 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :3, ht[1].size :8
- 1
圖示結果如下所示:
- 接下來我們的程式執行了刪除操作,dictDelete函式,實際上呼叫的是dictGenericDelete函式。
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
unsigned int h, idx;
dictEntry *he, *prevHe;
int table;
if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
h = dictHashKey(d, key);
for (table = 0; table <= 1; table++) {
idx = h & d->ht[table].sizemask;
he = d->ht[table].table[idx];
prevHe = NULL;
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
/* Unlink the element from the list */
if (prevHe)
prevHe->next = he->next;
else
d->ht[table].table[idx] = he->next;
if (!nofree) {
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
d->ht[table].used--;
return he;
}
prevHe = he;
he = he->next;
}
if (!dictIsRehashing(d)) break;
}
return NULL; /* not found */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
if (dictIsRehashing(d)) _dictRehashStep(d);
實際上也執行了ReHash步驟,這次將 0 號雜湊表上的剩餘3個key全部ReHash到了 1 號雜湊表上,這其實就是漸進式ReHash了,因為ReHash操作不是一次性、集中式完成的,而是多次進行,分散在增刪改查中,這就是漸進式ReHash的思想。
漸進式ReHash是指ReHash操作不是一次集中式完成的,對於Redis來說,如果Hash表的key太多,這樣可能導致ReHash操作需要長時間進行,阻塞伺服器,所以Redis本身將ReHash操作分散在了後續的每次增刪改查中。
說到這裡,我有個問題:雖然漸進式ReHash分散了ReHash帶來的問題,但是帶來的問題是對於每次增刪改查的時間可能是不穩定的,因為每次增刪改查可能就需要帶著ReHash操作,所以可不可以fork一個子程序去做這個事情呢?
-
繼續看程式碼,接下來通過
h = dictHashKey(d, key);
計算出index,然後根據有無進行ReHash確定遍歷2個Hash表還是一個Hash表。因為ReHash操作如果在進行的話,key不確定存在哪個Hash表中,沒有被ReHash的話就在0號,否則就在1號。 -
這次Delete操作成功刪除了key0,而且將 0 號雜湊表上的剩餘3個key全部ReHash到了 1 號雜湊表上,並且因為ReHash結束,所以將1號Hash表變成了0號雜湊表,如圖所示:
- 後續的刪除操作清除了所有的key,然後我們呼叫了
dictRelease(dd)
釋放了這個字典。
void dictRelease(dict *d)
{
_dictClear(d,&d->ht[0],NULL);
_dictClear(d,&d->ht[1],NULL);
zfree(d);
}
int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
unsigned long i;
/* Free all the elements */
for (i = 0; i < ht->size && ht->used > 0; i++) {
dictEntry *he, *nextHe;
if (callback && (i & 65535) == 0) callback(d->privdata);
if ((he = ht->table[i]) == NULL) continue;
while(he) {
nextHe = he->next;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
ht->used--;
he = nextHe;
}
}
/* Free the table and the allocated cache structure */
zfree(ht->table);
/* Re-initialize the table */
_dictReset(ht);
return DICT_OK; /* never fails */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
四、ReHash和漸進式ReHash
- Rehash:就是將原始Hash表(0號Hash表)上的Key重新按照Hash函式計算Hash值,存到新的Hash表(1號Hash表)的過程。
- 漸進式ReHash:是指ReHash操作不是一次性、集中式完成的,對於Redis來說,如果Hash表的key太多,這樣可能導致ReHash操作需要長時間進行,阻塞伺服器,所以Redis本身將ReHash操作分散在了後續的每次增刪改查中。
具體情況看上面例子。
五、ReHash期間訪問策略
Redis中預設有關Hash表的訪問操作都會先去 0 號雜湊表查詢,然後根據是否正在ReHash
決定是否需要去 1 號Hash表中查詢,關鍵程式碼如下(dict.c->dictFind()):
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; //根據這一句判斷是否需要在 1 號雜湊表中查詢。
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
五、遍歷
可以使用dictNext
函式遍歷:
dictIterator *i = dictGetIterator(dd); //獲取迭代器
dictEntry *de;
while ((de = dictNext(i)) != NULL) { //只要結尾不為NULL,就繼續遍歷
printf("%s->%s\n",(char*)de->key, (char*)de->v.val);
}
Out >
key3->val3
key2->val2
key1->val1
key5->val5
key0->val0
key4->val4
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
有關遍歷函式dictSacn()
的演算法,也是個比較難的話題,有時間再看吧。
六、總結
這篇文章主要分析了dict的資料結構、建立、擴容、ReHash、漸進式ReHash,刪除等機制。只是單純的資料結構的分析,沒有和Redis一些機制進行結合對映,這方面後續再補充,但是已經是一篇深度好文了 :)。