REDIS的rehash机制

已经说过,redis整个数据结构是一个hash。在运行中,有两个hashtable,但是只有一个是在使用中的,另一个是备用处理rehash之用。

 首先说明下redis对于sds(也就是redis中key的数据类型)的hash算法为:

 

while (len--)
    hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */
    return hash;
}

 

 

 是很典型的字符串hash方式,不详述。下面的hash处理是重点:

 

    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }

 

 

 这里提到了一个重要的变量:sizemask。在其他文章已经提到过(http://askdba.alibaba-inc.com/libary/control/getArticle.do?articleId=30561),redis的table(也就是用来存储所有键值的hashtable),大小永远是2的幂次方。而sizemask则是tableSize - 1. 也就是说,sizemask的二进制表现就是一串1。所以,上述的算法就是:获得hash之后用tableSize取余。这点也是和我们的预期一样的。

 说完hash的机制,然后说下rehash的触发机制,也就是rehash的临界点。

 

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

 

 

 首先dictExpand的第二个参数明确显示每次table大小的扩容都是以2的倍数为大小的。其次,显示的触发条件有两个(逻辑与关系)

    • key的总数大于tableSize

    • slot的平均key数大于临界值

 在这两个条件下,触发扩容然后rehash移动键值。至于rehash的具体算法,在《REDIS介绍》已经介绍过,详细代码也不用贴。本文侧重于rehash的运行机制。

 在其他文章也说过,redis的事件机制是单线程的。而rehash操作是timeEvent中的一个步骤(包含在serverCron中的databasesCron任务)。那么问题来了,当redis的键值非常大的时候,比如说1G(redis运行内存1G是很正常的事情),那么也就意味着一次rehash操作就得耗费秒级别的时间,由于是单线程的,就势必会阻塞了连接处理操作,对外表现就是服务拒绝。所以,redis是不可能也不允许全量rehash的,取而代之的是,redis采用了时间分片操作。

 在实际操作中,redis采用了毫秒分片,也就是说,在每个serverCron里面,如果需要或者正在rehash,那就分2ms进行rehash,那么这个2ms是怎么分配的呢:

int incrementallyRehash(int dbid) {
    /* Keys dictionary */
    if (dictIsRehashing(server.db[dbid].dict)) {
        dictRehashMilliseconds(server.db[dbid].dict,1);
        return 1; /* already used our millisecond for this loop... */
    }
    /* Expires */
    if (dictIsRehashing(server.db[dbid].expires)) {
        dictRehashMilliseconds(server.db[dbid].expires,1);
        return 1; /* already used our millisecond for this loop... */
    }
    return 0;
}

 

 

 可以看出,redis对于持久KV分配1ms,对Expire KV分配1ms,从中可以看出,redis将持久KV和Expired KV进行分开存储。这个细节这里不详述。考虑到serverCron的频率一般在10-100,也就是说,一般情况下,rehas对于吞吐率的影响在1% - 10%(吞吐率:对外服务时间/运行时间)。考虑到用户传输层的额外开销,这点影响是忽略不计的。

 回到正题,redis是如何控制着1ms的呢

 

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }

 

 

 也就是说,每次调用dictRehash操作,然后检查时间,看是否超时。这里就有个问题,如果一次性调用dictRehash超过1ms怎么办?

 在《REDIS介绍》提到过,redis的rehash迁移是按照slot来做的,也就是table中的每一行。上文中的dictRehash(d,100)中的100就是每次rehash的slot数。也就是说,redis默认每次rehash迁移100个slot所用的时间少于1ms。由于在slot中没有特定的变量来标记rehash的进度,所以redis的rehash最小粒度是slot。考虑到slot中的KV是个链表,多个KV分配到多个slot和分配到单个slot的rehash是无明显差别的(反正都是内存的随机访问),而redis的值是放在堆中的,在table中只存有指向这个值的内存指针。也就是说,redis在rehash的时候,仅仅是移动整个KV的数据结构也就是dictEntry的结构指针。所以,这个效率是非常高的。换句话说,value哪怕是200M的数据,移动的时候也和1K的数据没有半毛钱差别。可以得出结论:除了rehash的时候计算hash值之外,其余都是小变量的copy操作,时间损耗非常小而且非常稳定。

  换言之,以下的代码就是唯一的造成单位rehash操作时间不稳定的原因:

 

 

h = dictHashKey(d, de->key) & d->ht[1].sizemask;

 

 或者说,key的长度会直接正向影响rehash性能。至于有多影响,我觉得仁者见仁智者见智了。

 当然,在rehash中还有很多细节问题你:比如在两次rehash中插入了大量的KV,或者删除了大量的KV。这些细节读者可以自己读源码去发掘。在此不表。

 最后,redis的cluster支持中对于KEY的多node分配,也是采用差不多的机制,不过其中有一点比较麻烦的是:如果KEY在本机的slot中未找到,应该去哪台机器上找?毕竟在rehash中,在table1中找不到,那就可以在table2中找,两个table都找不到那就肯定没有。cluster的实现则是redis又一个亮点。下篇文章再详述。

 

From:鱼藏

 

 

你可能感兴趣的:(REDIS的rehash机制)