redis中的字典就跟正常语言中的hash差不多,同样需要计算key来索引,同样需要解决冲突问题等。
代码位于src/dict.c|h中
dictEntry结构体
字典的基本组成是dictEntry结构体:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
其中key类型是void*,估计还是因为多态吧?后面再研究这个问题。
v是一个union,是具体的值;
为了解决键冲突,dictEntry本质是个单向链表,遇到冲突的时候,会把冲突键值对放在同一个索引的链表中,所以hash的查找不一定是O(1)。
dictht结构体
这是一个完整的字典,字典中的键值对存在这个结构体指定的地方。
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
首先,dictht结构体中有一个dictEntry数组,用来存储键值对,而每一个键值对是一个单向链表;
size 表示dictEntry数组的大小;
sizemask是一个比size正好小1的数字,个人猜测是字典的操作中几乎每次都跟索引查询有关,所以单独保存这个值以提高效率,不知道是不是这样;
最后,used表示当前哈希表中已经有了多少个dictEntry,这个值与size一起构成哈希表进行扩缩容的重要指标。
dict结构体
有意思的地方在于上面的两个结构体都不是对外的,实际上字典对外的结构体是dict:
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;
从这个结构体可以看到几个信息:
- 每个字典都有一个dictht二元数组,即ht成员,正常情况下字典的增删改查都在h[0]上进行,而h[1]大部分时候都是空指针;但当字典需要扩缩容时,即所谓的rehash,就要使用h[1],直到rehash完成,h[1]会重新置为空;
- 字典有一个rehashidx属性,这个属性用来标记字典当前是否在rehash;redis字典的rehash不是连续完成的,而是分阶段进行;正常情况下这个值都为-1表示没有进行rehash,当进入rehash状态时,这个值会从0开始计数,直到其值与ht[0]的size相同,此时rehash完成,值会重新变为-1;
- 字典有一个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;
目测就是字典计算key的哈希值用的;
- 字典有一个privdata成员,目前还没有发现其作用,不过从代码上看,在计算key时会用到,具体使用可能得到redis的server中查看;
- 有一个iteratirs成员,对字典迭代时使用,具体使用需要继续研究;
重要属性
字典初始大小
#define DICT_HT_INITIAL_SIZE 4
这个标记了一个字典刚生成时的size;
dict_can_resize
static int dict_can_resize = 1;
这个值标记字典当前是否可以被rehash,1表示可以,0表示不行;不过作者也说了,即使为0也并不能完全阻止rehash,因为下面的另一个变量;
dict_force_resize_ratio
static unsigned int dict_force_resize_ratio = 5;
这个值是used与size的比值的阈值,当比值超过这个值时,即使上面的值设置为0,也是可以rehash的;
dict_hash_function_seed
static uint8_t dict_hash_function_seed[16];
- 这个值似乎是当随机返回键值对时使用,可能有别的用处,尚待继续研究;
接口
创建新字典
dict *dictCreate(dictType *type,
void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type,privDataPtr);
return d;
}
压缩字典
int dictResize(dict *d)
{
unsigned long minimal;
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
虽然函数叫做dictResize,但从代码中可以看到,这里没有扩而只有缩,它会找到一个比当前size小但是又刚好比字典size大的第一个2的次方数,将字典的size设置为这个值,然后将字典resize;
扩大字典容量
int dictExpand(dict *d, unsigned long 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;
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size); // 找到第一个满足要求的2的次方幂;
/* 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;
}
这个是用来扩大字典容量的方法,需要注意的地方是它根据字典是初始化还是真正的rehash进行不同的操作;上面的dictCreate函数只是创建出dict和它的两个dictht成员,但是h[0]的table初始化时是空指针,因此这里就需要有个指向实际表的过程;
字典rehash过程
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) {
uint64_t 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;
}
这个函数执行字典的rehash,虽然长,但过程很简单,就是不停的对h[0]中的每一项进行遍历,查看它是否为空,如果不为空,就对这个链表上的每个键值对重新哈希到h[1]上,哈希完毕后将h[0]指向h[1],而h[1]继续为空,指针就是好啊;
这里可以看到rehashidx的使用,需要注意几个地方:
1 rehashidx是有符号的,而size却是无符号的,所以断言时必须转换类型;
2 rehashidx这里显示了它的实际用处,那就是标记目前对表0rehash到了哪个位置,因为rehash是个不断迭代的过程,这里能看到,当迭代进行到一定时间时,就会被终止,而rehashidx记录了本次终止时table被执行到了哪个索引处,下次继续时就不需要从头开始了;另外由于做了断言,也不会发生溢出;
- 有一个问题没有想明白,为什么这里会同时有n和empty_visits两个计数器,理论上只要有后者就够了,为什么前者也一直在循环?
在给定时间内执行rehash
int dictRehashMilliseconds(dict *d, int ms) {
if (d->iterators > 0) return 0;
long long start = timeInMilliseconds();
int rehashes = 0;
while(dictRehash(d,100)) {
rehashes += 100;
if (timeInMilliseconds()-start > ms) break;
}
return rehashes;
}
字典插入键值对
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. */
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;
}
它会调用下面的dictAddRaw函数,它将键值对插入字典中,如果键已经存在,就更新相应的值;按照作者的意思,dictAddRaw也是一个用户可以直接调用的函数,它会查询key是否已经在字典中,如果key一ing存在,就将相应的dictEntry存储在existing中,这样的好处是用户一旦发现已经有键,可以直接更新值而不是什么都做不了或者还需要先删除再操作;需要记住,addRaw只有两种情况,如果键已经存在就返回空指针,但是existing中会存储键值对的指针;如果不存在就会新建一个键值对出来并将其指针返回;
还要注意的一点是,函数被调用时,它会判断字典当前是否处在rehash中,如果是,则会先执行一些rehash动作,很多函数中都有这个操作;
替换键值对的值
int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, *existing, auxentry;
/* Try to add the element. If the key
* does not exists dictAdd will succeed. */
entry = dictAddRaw(d,key,&existing);
if (entry) {
dictSetVal(d, entry, val);
return 1;
}
auxentry = *existing; // 从这里开始要注意设置新值以及删除旧值的顺序,不能颠倒;
dictSetVal(d, existing, val);
dictFreeVal(d, &auxentry);
return 0;
}
这个方法会调用addRaw去查询键是否已经存在于字典中,注意上面提到的addRaw返回值的情况,如果返回非空,说明创建了一个新的键值对,此时只要设置值即可;如果返回空,说明要更新值,而地址存储在existing中;
添加新键值对或者查询key对应的键值对
dictEntry *dictAddOrFind(dict *d, void *key) {
dictEntry *entry, *existing;
entry = dictAddRaw(d,key,&existing);
return entry ? entry : existing;
}
没什么,就是单纯调用addRaw,要不返回新添加的键值对,要不返回已经存在的键值对;
删除键值对
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
uint64_t 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 */
}
查询键是否已经存在于字典中,如果查询到就将其从字典中移除,然后根据情况决定是否需要删除键和值;不过这个方法只是个对内的方法,是其它方法的base;
真正的删除键值对方法
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
调用上面这个函数去执行键值对的删除,这个函数设置的nofree是0,意味着它会将key和value的空间真正删除掉并忽略返回的键值对;之所以这么做,是因为有时候查找到键值对后需要立马执行删除,而有时候不需要,因此上面的dictGenericDelete函数就做了包了一层的动作;
unlink
dictEntry *dictUnlink(dict *ht, const void *key) {
return dictGenericDelete(ht,key,1);
}
/* You need to call this function to really free the entry after a call
* to dictUnlink(). It's safe to call this function with 'he' = NULL. */
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
if (he == NULL) return;
dictFreeKey(d, he);
dictFreeVal(d, he);
zfree(he);
}
不太好翻译这个函数名,作者的意思是有时候查找到键值对后要删除,但是删除前会执行一些其它动作,执行完毕才删除,因此出现了上面的unlink函数,它返回那个查到的键值对,但步删除其中的项,而是先让用户执行一些操作,但是最后调用方必须记得调用下面的dictFreeUnlinkedEntry函数,否则怕是得内存泄漏,这两个函数应该是成双成对出现的;
清除整个字典
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 */
}
/* Clear & Release the hash table */
void dictRelease(dict *d)
{
_dictClear(d,&d->ht[0],NULL);
_dictClear(d,&d->ht[1],NULL);
zfree(d);
}
_dictClear函数将字典的指定的table置为空,释放所有键值对的空间;
dictRelease则通过调用_dictClear函数的方式释放掉两个表,然后再释放掉dict本身占用的空间;
查找键是否已经存在于字典中以及键对应的值
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
uint64_t h, idx, table;
if (dictSize(d) == 0) return NULL; /* dict is empty */
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];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
if (!dictIsRehashing(d)) return NULL;
}
return NULL;
}
void *dictFetchValue(dict *d, const void *key) {
dictEntry *he;
he = dictFind(d,key);
return he ? dictGetVal(he) : NULL;
}
从字典中随机返回一个键
dictEntry *dictGetRandomKey(dict *d)
{
dictEntry *he, *orighe;
unsigned long h;
int listlen, listele;
if (dictSize(d) == 0) return NULL;
if (dictIsRehashing(d)) _dictRehashStep(d);
if (dictIsRehashing(d)) {
do {
/* We are sure there are no elements in indexes from 0
* to rehashidx-1 */
h = d->rehashidx + (random() % (dictSlots(d) - d->rehashidx));// 保证键从有效的区间开始算起
he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
d->ht[0].table[h];
} while(he == NULL);
} else {
do {
h = random() & d->ht[0].sizemask;
he = d->ht[0].table[h];
} while(he == NULL);
}
listlen = 0;
orighe = he;
while(he) {
he = he->next;
listlen++;
}
listele = random() % listlen;
he = orighe;
while(listele--) he = he->next;
return he;
}
唯一需要注意的是当处在rehash模式时,因为rehashidx记录了表0中已经被清空的索引,所以获取键时可以直接跳过这些区间,然后在表0和表1中剩下的所有区间中去随机返回,其中的dictSlots是一个宏定义,用来计算两个表当前总的size的大小;