在redis中,键值以哈西表的方式进行存储,在键值对的数目比较多时,哈西值冲突的次数就会变多,这会降低检索效率。为了减少哈西表中的地址冲突次数,redis会增加键值空间,重新定义键值对的映射地址,也就是进行所谓的rehash。本文主要通过redis源码分析rehash原理。主要解决如下几个问题。
1,redis中hash表的数据结构是怎样的?
2,什么时候开始进行rehash?
3,怎样为扩充的键值对分配内存?
4,具体的rehash步骤?
在redis中,与hash表存储有关的数据结构主要有3个,分别为dictEntry、dictht、dict。其中dictEntry存储键值对,dictht为包括dictEntry的hash表,dict为定义了两个dictht的字典(2个dictht是为了进行rehash),它们的关系如下:
在dictEntry中存储了key、v以及指向下一个entry的next指针(链地址法);
在dictht中定义了二重指针table,指向dictEntry的指针个数为size,sizemask用于联合哈市函数计算哈西值,used用于记录哈西表中存储的键值对个数;
dict中与rehash有关的数据有两个,ht[2]表示两个哈西表,其中后者作为rehash过程中的临时哈西表,起过度作用。而rehashidx记录ht[0]中已经进行rehash的下标,如果值为-1表示没有进行rehash。
rehash和hash表空间扩充是同步的,一旦hash表的空间进行了扩充,就需要进行rehash。所以什么时候会进行hash表空间扩充呢,当然是在向hash表中添加元素的时候了。函数为dictAdd。
/* 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;
}
其中主要的函数为dictAddRaw,会首先利用_dictExpandIfNeeded判断是否进行空间扩展,整个dictAdd函数流程如下:
hash表空间扩展的3个条件:
1、空哈西表;
2、used/size>radio;
3、正在进行rehash(此时只返回标志位,不进行实际hash表空间扩充工作,所以实际空间扩展只有以上两个条件);
进行空间扩展之后,将rehashidx从-1更改为0,表示从0开始进行将ht[0]中的键值对转移到ht[1]。
如下,在dictAddRaw中,rehash被如下的方法调用:
if (dictIsRehashing(d)) _dictRehashStep(d);
由于dictIsRehashing的实际操作是根据rehashidx进行判断的,所以可以将rehashidx设置0视为rehash的触发。
1、需要为ht分配多少个键值指针?
2、有两个ht,应该为哪个ht分配空间呢?
在redis中,每次为expand的大小为2×used个(初始为4),为了保证hash表的hashmask全为1,所以分配的指针数为2的指数次方(_dictNextPower)。
ht[0]是当前使用的hash表,因为它不够用了,所以才创建一个更大空间的ht[1],用于将ht[0]中的内容转移过来。
/* Expand or create the hash table */
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);
/* 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;
}
函数接口如下:
int dictRehash(dict *d, int n)
1、形参n表示需要将ht[0]中几个不为空的table指针数组所对应的元素搬移到ht[1]中。如果ht[0]的table指针数组有太多空,使程序在此函数逗留太长时间的情况。程序设置empty_visits=10×n阈值,如果连续查找是空指针次数超过此阈值,就会跳出此程序;
2、rehash会从rehashidx开始将指针下的bucket(就是table指针数组指向的链表)中的所有entry重新进行哈西计算,将得到的key作为ht[1]的索引值,并将entry中的value放在此处(以在链表头插入元素的方式);
3、如果已经将ht[0]中的所有entry转移到ht[0]中,需要释放ht[0]中的table指向的size个指针,然后将ht[1]复制给ht[0],并将ht[1]置位,将ht[0]的rehashidx置位,表示rehash已经完成。
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
//查找n个entry
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 */
//保证一次rehash查找指针数组中的空指针不能超过empty_visits
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 */
//将链表中的所有entry重新进行hash映射,移至ht[1]中。
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);//释放指针数组,也就是table指向的size个指针
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
如果hash表中的entry比较多的话,一次完成rehash需要耗费大量的时间,阻塞其它函数操作。所以,在redis中,采用了一种渐进rehash的方法进行rehash,就是每次转移1个,分步转移ht[0]中的entry。redis中设定在执行add、delete、find操作时,执行一次dictRehash(d,1)。