通过上一篇对dictScan函数的分析,我们引出了两个问题,就是Redis字典在进行扩容的时候,会从size=8直接扩容到size=64吗?那段代码块真的有用吗?下面我们就通过查看源码,逐步来探索一下这个问题。
想要探索这个问题的答案,我们首先要看一下字典会在什么时候进行扩容,首先查看到的函数是:
* 根据需要,初始化字典(的哈希表),或者对字典(的现有哈希表)进行扩展
* T = O(N)
static int _dictExpandIfNeeded(dict *d)
{
// 渐进式 rehash 已经在进行了,直接返回
if (dictIsRehashing(d)) return DICT_OK;
// 如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
// T = O(1)
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
// 一下两个条件之一为真时,对字典进行扩展
// 1)字典已使用节点数和字典大小之间的比率接近 1:1
// 并且 dict_can_resize 为真
// 2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
// 新哈希表的大小至少是目前已使用节点数的两倍
// T = O(N)
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}
从上面的代码和注释中,我们知道,_dictExpandIfNeeded函数的作用是:根据需要,初始化字典(的哈希表),或者对字典(的现有哈希表)进行扩展。
在满足两个条件之一,准备对字典进行扩容时,会调用到dictExpand函数,该函数有一个重要的参数,就是后面的 d->ht[0].used*2 参数,这边决定了新哈希表的大小至少是目前已使用节点数的两倍,到这里,我想我们的疑问基本就被解决了。当字典一时间内进行大量的键值对插入操作时,这个used值就会迅速的增大,导致字典在做扩容的时候字典的size值也变得很大,就可能从size=8直接扩容到size=64
但是,在查看dictExpand函数的时候发现问题并没有那么简单:
* 创建一个新的哈希表,并根据字典的情况,选择以下其中一个动作来进行:
* 1) 如果字典的 0 号哈希表为空,那么将新哈希表设置为 0 号哈希表
* 2) 如果字典的 0 号哈希表非空,那么将新哈希表设置为 1 号哈希表,
* 并打开字典的 rehash 标识,使得程序可以开始对字典进行 rehash
* size 参数不够大,或者 rehash 已经在进行时,返回 DICT_ERR 。
* 成功创建 0 号哈希表,或者 1 号哈希表时,返回 DICT_OK 。
* T = O(N)
*/
int dictExpand(dict *d, unsigned long size)
{
// 新哈希表
dictht n; /* the new hash table */
// 根据 size 参数,计算哈希表的大小 !!!!!!!!!!!!!!!!!!!!!!!!
// T = O(1)
unsigned long realsize = _dictNextPower(size);
// 不能在字典正在 rehash 时进行
// size 的值也不能小于 0 号哈希表的当前已使用节点
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
// 为哈希表分配空间,并将所有指针指向 NULL
n.size = realsize;
n.sizemask = realsize-1;
// T = O(N)
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
// 如果 0 号哈希表为空,那么这是一次初始化:
// 程序将新哈希表赋给 0 号哈希表的指针,然后字典就可以开始处理键值对了。
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
}
从上面的代码我们可以看到,在 调用dictExpand函数时,虽然会传入一个之前算过的 d->ht[0].used*2 ,但是在真正创建新的哈希表的时候,还要根据_dictNextPower函数来计算字典的realsize,也就是下面的代码:
realsize = _dictNextPower(size)
我们再接着来看一下 _dictNextPower 函数是怎么定义的,干了些啥:
* 计算第一个大于等于 size 的 2 的 N 次方,用作哈希表的值
* T = O(1)
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE=4;
if (size >= LONG_MAX) return LONG_MAX;
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
该函数的作用就是用来计算第一个大于等于 size 的 2 的 N 次方的值,并将其用作要创建的哈希表的 realsize 。
那为什么要这么做呢??直接用 d->ht[0].used*2 不就可以直接创建一个大小够用的哈希表了吗?
其实这么做,是为了控制哈希表在扩容的时候,正好是 4 的2倍数。因为最开始的时候哈希表的大小为4,只有保证每次扩容都是 4 的2倍数,这样的话才能保证字典在做rehash的时候还是会遵循之前的迁移方式,比如从0->4,1->5,或者从0->8,1->9,2->10,(感觉这是由于计算机底层的二进制计算规则决定的,只有是4的2倍数,才方便进行一些运算(这只是我自己的感觉,大家有更有道理的验证的话欢迎留言评论~ ))
代码看到这里,也就回答了上一篇博客留下来的两个问题:
- 字典在扩容的时候是有可能从size=8直接扩容到size=64的情况的。就是在短时间内插入大量的键值对,导致哈希表的used值变的很大,最后传给dictExpand函数的size突然增大,新建的哈希表一下子会扩大很多。
- 上一篇博客 dictScan 函数中的 do … while 循环内的代码是有用的,有大大滴用滴,Redis的作者是很厉害滴!