redis的rehash工作原理

在redis中,键值以哈西表的方式进行存储,在键值对的数目比较多时,哈西值冲突的次数就会变多,这会降低检索效率。为了减少哈西表中的地址冲突次数,redis会增加键值空间,重新定义键值对的映射地址,也就是进行所谓的rehash。本文主要通过redis源码分析rehash原理。主要解决如下几个问题。

1,redis中hash表的数据结构是怎样的?

2,什么时候开始进行rehash?

3,怎样为扩充的键值对分配内存?

4,具体的rehash步骤?

1,redis中hash表的数据结构

在redis中,与hash表存储有关的数据结构主要有3个,分别为dictEntry、dictht、dict。其中dictEntry存储键值对,dictht为包括dictEntry的hash表,dict为定义了两个dictht的字典(2个dictht是为了进行rehash),它们的关系如下:

redis的rehash工作原理_第1张图片

在dictEntry中存储了key、v以及指向下一个entry的next指针(链地址法);

在dictht中定义了二重指针table,指向dictEntry的指针个数为size,sizemask用于联合哈市函数计算哈西值,used用于记录哈西表中存储的键值对个数;

dict中与rehash有关的数据有两个,ht[2]表示两个哈西表,其中后者作为rehash过程中的临时哈西表,起过度作用。而rehashidx记录ht[0]中已经进行rehash的下标,如果值为-1表示没有进行rehash。

2,redis什么时候进行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函数流程如下:

redis的rehash工作原理_第2张图片

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的触发。

3,怎样为扩充的键值对分配内存?

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;
}

4,rehash步骤

函数接口如下:

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)。

你可能感兴趣的:(C,redis)