通过上一篇对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倍数,才方便进行一些运算(这只是我自己的感觉,大家有更有道理的验证的话欢迎留言评论~ ))

代码看到这里,也就回答了上一篇博客留下来的两个问题:

  1. 字典在扩容的时候是有可能从size=8直接扩容到size=64的情况的。就是在短时间内插入大量的键值对,导致哈希表的used值变的很大,最后传给dictExpand函数的size突然增大,新建的哈希表一下子会扩大很多。
  2. 上一篇博客 dictScan 函数中的 do … while 循环内的代码是有用的,有大大滴用滴,Redis的作者是很厉害滴!