字典dict
redis中的字典,即hash表,其實現與Java中的HashMap基本類似。同樣是基於數組和鏈表的,通過“拉鍊法”來處理碰撞。
字典中的每一個key都是獨一無二的,當要加入一個key-value對時,如果key在字典中已經存在,則會直接返回,而不會重新將其加入到字典中。
字典的實現
hash節點的定義如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
- key:元素的key
- v:元素的值
- next:處理碰撞,所有分配到同一索引的元素通過next指針鏈接起來形成鏈表
key和v都可以保存多種類型的數據
hash表的定義如下:
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
- table:一個二級指針,真正存儲數據的地方。可以將table看做一個指向數組的指針,而數組就是hash表最基本的結構。通過數組和hash節點中的next指針形成完整的hash表。
- size:數組的大小,通常爲2的整數次方
- sizemask:hash表大小掩碼,用於計算索引,當size非0時爲(size-1)
- used:hash表中已有節點的數量
處理函數
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;
redis中通過dictType這樣的一個結構用來存儲針對不同類型的鍵值對的處理函數,這樣對於不同類型的鍵值對,就可以有不同的處理了。即通過函數指針實現多態。
字典定義如下:
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;
- type:處理函數表,通過其中保存的函數指針對不同類型的數據進行不同的處理,實現多態
- privdata:私有數據
- ht[2]:hash表,可以發現一個字典中有兩個hash表
- rehashidx:ht[0]中正在rehash的桶的索引,當rehash=-1時,表明此時沒有在進行rehash操作
- iterator:正在運行的迭代器的數量
示意圖
一個字典的示意圖如下:
rehash
hash表利用負載因子loadfactor = used/size來表明hash表當前的存儲情況。
當負載因子過大時操作的時間複雜度增大,負載因子過小時說明hash表的填充率很低,浪費內存。redis中的數據都是存儲在內存中的,因此我們必須儘量的節省內存。
因此我們必須將loadfactor控制在一定的範圍內,同時保證操作的時間複雜度接近O(1)和內存儘量被佔用。即rehash操作分爲擴展和收縮兩種情況。
dict中有兩個hash表,ht[0]和ht[1]。所有的數據都是存在放dict的ht[0]中,ht[1]只在rehash的時候使用。dict進行rehash的時候,將ht[0]中的所有數據rehash到ht[1]中。
dict的rehash並不是一次性完成的,而是分多次、漸進式的完成的。
每一步的大小分爲兩種:
1)在一步中將ht[0]的table中的一個元素,也就是一個哈希桶所對應的鏈表中的所有元素進行rehash。(在調用dict的一些操作函數時,如add,find等時進行)
2)在一步中執行一段固定的時間,當時間到達後,暫停rehash。 (數據庫週期性執行databasesCron()時進行)
這兩種方法對應的函數分別是_dictRehashStep和dictRehashMilliseconds。
rehash過程的基本步驟如下:
1) 首先調用dictExpand爲ht[1]分配空間
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE;
if (size >= LONG_MAX) return LONG_MAX;
while(1) {
if (i >= size) //size爲第一個大於等於size的2的冪次
return i;
i *= 2;
}
}
int dictExpand(dict *d, unsigned long size)
{
dictht n;
unsigned long realsize = _dictNextPower(size); //計算ht[1]所需大小
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
if (realsize == d->ht[0].size) return DICT_ERR;
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
if (d->ht[0].table == NULL) { //表明爲第一次加入元素時創建hash表
d->ht[0] = n; //直接將新創建的hash表賦給ht[0]
return DICT_OK;
}
d->ht[1] = n; //否則爲rehash,將新創建的hash表賦給ht[1]
d->rehashidx = 0;//表明正在處於rehash過程中
return DICT_OK;
}
2)調用dictRehash逐步完成rehash操作
int dictRehash(dict *d, int n) {
int empty_visits = n*10; //每次rehash最多可訪問的空桶的個數
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
//rehashidx用於指示正在對ht[0]中的第幾個桶進行rehash操作,確保不會越界
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]; //ht[0]中正在rehash的桶元素的頭節點
while(de) {
unsigned int h;
nextde = de->next;
h = dictHashKey(d, de->key) & d->ht[1].sizemask; //計算ht[0]中元素進行rehash後在ht[1]中的索引
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++; //該桶處理完成後,準備處理下一個桶
}
//ht[0]剩餘元素個數爲0,表明ht[0]中的元素已經全部rehash到ht[1]中,因此rehash過程已經完成。
if (d->ht[0].used == 0) {
//可以釋放ht[0],並將ht[1]賦給ht[0]後重置ht[1]
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1; //表明rehash已經結束
return 0;
}
return 1; //否則還處於rehash過程中
}
將ht[0]中的所有元素全部轉移到ht[1]中,釋放原來的ht[0],將ht[1]賦給ht[0],並重置ht[1]爲下次rehash做準備
創建一個字典:
dict *dictCreate(dictType *type,void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d)); //分配內存
_dictInit(d,type,privDataPtr); //字典初始化
return d;
}
int _dictInit(dict *d, dictType *type,void *privDataPtr)
{
_dictReset(&d->ht[0]); //初始化兩個hash表
_dictReset(&d->ht[1]);
d->type = type;
d->privdata = privDataPtr;
d->rehashidx = -1;
d->iterators = 0;
return DICT_OK;
}
static void _dictReset(dictht *ht)
{
//hash表的初始化
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}
向字典中加入一個元素:
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key);
if (!entry) return DICT_ERR; //表明key已存在
dictSetVal(d, entry, val);
return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
if (dictIsRehashing(d)) _dictRehashStep(d); //當正在rehash時,先進行rehash操作
if ((index = _dictKeyIndex(d, key)) == -1) ///計算key在hash表中的桶的索引,-1時表示key已存在
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++;
dictSetKey(d, entry, key);
return entry;
}
可以發現,在一個字典中,每一個key都是獨一無二的。當加入一個key-value對時,如果字典中已存在該key,則會直接返回而不會繼續執行加入操作,既不會執行dictSetKey(),也不會執行dictSetVal()。
本文所引用的源碼全部來自Redis3.0.7版本
redis學習參考資料:
https://github.com/huangz1990/redis-3.0-annotated
Redis 設計與實現(第二版)