redis6.0.5之dict阅读笔记3-dict之新增元素中空间扩展和平滑Rehashing
******************************************************************
在上一节中我们进行了新增元素的操作,新增的元素当然需要一个地方存放,
今天我们就先来看看redis是如何来扩容的
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK; //如果正在做Rehashing,不做扩容
/* If the hash table is empty expand it to the initial size. */
如果hash表示空的,需要初始化,初始化的大小为DICT_HT_INITIAL_SIZE(4)
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* 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. */
如果我们使用元素和桶额比例已经达到了1:1,并且允许扩容hash表或者虽然不允许扩容,
但是元素和桶的比例超过安全阀值dict_force_resize_ratio(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;
}
******************************************************************
* 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 */
如果正在做Rehashing或者已使用元素比将要分配的空间的大,返回错误
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. */
这里分两种情况,一种是第一次初始化,另外一种是需要做rehashing,
对于第一次初始化的情况,我们设置第一个是hash表,让它可以接收键值
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
如果是要做rehashing,那么我们就初始化第二个hash表,准备做增量(平滑)rehashing
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0; //设置标志位
return DICT_OK;
}
******************************************************************
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE;
//LONG_MAX == long int
//LLONG_MAX == long long int
if (size >= LONG_MAX) return LONG_MAX + 1LU; //如果超过有符号数,那么使用无符号数
while(1) { //一直翻倍直到大于给定长度size为止,所以返回的长度肯定是2的倍数,初始值为2的2次方,后面又是2的指数
if (i >= size)
return i;
i *= 2;
}
}
******************************************************************
我们再来看看redis是如何做平滑rehashing的 ?
一种是按时间,一种是按个数
我们先看按个数的,如下
******************************************************************
/* This function performs just a step of rehashing, and only if there are
* no safe iterators bound to our hash table. When we have iterators in the
* middle of a rehashing we can't mess with the two hash tables otherwise
* some element can be missed or duplicated.
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used. */
这个函数仅仅是rehashing的一步,而且只能在没有绑定非安全迭代器的情况下执行。
当在做rehashing过程中使用迭代器,我们不能混淆两个hash表,否则一些元素可能被遗漏或者重复
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1); //调用dictRehash做rehashing,参数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. */
在平滑rehashing中执行N步,返回1表示好需要继续从老的hash表到新的hash表,否则如果是0的话,表示已经迁移完毕。
注意一步rehashing包含一整个桶从老的hash表到新的hash表的迁移(一个桶可能拥有超过一个键值的元素,因为我们使用的是链表存储),
然而因为一次最多只访问N*10个桶,而hash表的部分桶是由空的空间组成的,没有元素,所以它(这个函数)不能保证至少一个桶可以被迁移。
另外一方面,如果总量不控制的话(一直要查找到有元素的桶位置,远远大于 n*10个桶),那这个函数可能阻塞很长一段时间,耽误时间
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0; //如果不在做rehashing,就直接返回
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) {
//查找一个非空桶的位置,直到找到一个位置或者超过了要迁移个数的10倍(为了让阻塞时间少点),就停止
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]; //将原来table1表中对应桶的第一个元素挂在迁移元素后面
d->ht[1].table[h] = de; //将迁移的元素放在table1中的第一个位置,得到 新元素->原来的桶链表
d->ht[0].used--; //表table0减少一个元素
d->ht[1].used++; //表table1增加一个元素
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); //搬迁完毕之后需要释放table0申请的空间
d->ht[0] = d->ht[1];
//将table1的值赋值给table0,这里的拷贝是浅拷贝,所以对于指针 **table,只拷贝了对应的地址,内容不拷贝
_dictReset(&d->ht[1]);//将table1清空,这里不需要释放为table分配的内存空间,因为上一句中已经给了table0了
d->rehashidx = -1; //设置rehashing结束标志
return 0; //迁移完毕
}
/* More to rehash... */
return 1;
}
******************************************************************
再来看按照时间的迁移,如下
/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
用1毫秒的时间做平滑迁移
int dictRehashMilliseconds(dict *d, int ms) {
long long start = timeInMilliseconds(); //获取开始毫秒数
int rehashes = 0;
while(dictRehash(d,100)) { //迁移100桶,实际不一定,怕查找耽误过长时间
rehashes += 100; //成功迁移桶数加起来
if (timeInMilliseconds()-start > ms) break; //如果时间到了,就停止迁移
}
return rehashes; //返回总的迁移桶数
}
这里是获取时间的函数
long long timeInMilliseconds(void) {
struct timeval tv;//来自 time.h
/*struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};*/
gettimeofday(&tv,NULL); //获取当前时间
return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000); //秒和微妙全部转化成毫秒
}