Redis源码剖析-dict遍历算法

来源:互联网 发布:铁血战士知乎 编辑:程序博客网 时间:2024/06/06 11:49

先贴一下整体的代码

unsigned long dictScan(dict *d,                       unsigned long v,                       dictScanFunction *fn,                       dictScanBucketFunction* bucketfn,                       void *privdata){    dictht *t0, *t1;    const dictEntry *de, *next;    unsigned long m0, m1;    // 跳过空字典    if (dictSize(d) == 0) return 0;    // 如果没有rehash,迭代一个哈希表的字典    if (!dictIsRehashing(d)) {        t0 = &(d->ht[0]);        m0 = t0->sizemask;        /* Emit entries at cursor */        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);        de = t0->table[v & m0];        while (de) {            next = de->next;            fn(privdata, de);            de = next;        }    // 迭代有两个哈希表的字典    } else {        t0 = &d->ht[0];        t1 = &d->ht[1];        /* Make sure t0 is the smaller and t1 is the bigger table */        // 确保 t0 比 t1 要小        if (t0->size > t1->size) {            t0 = &d->ht[1];            t1 = &d->ht[0];        }        m0 = t0->sizemask;        m1 = t1->sizemask;        /* Emit entries at cursor */        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);        de = t0->table[v & m0];        while (de) {            next = de->next;            fn(privdata, de);            de = next;        }        /* Iterate over indices in larger table that are the expansion         * of the index pointed to by the cursor in the smaller table */        do {            /* Emit entries at cursor */            if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);            de = t1->table[v & m1];            while (de) {                next = de->next;                fn(privdata, de);                de = next;            }            /* Increment bits not covered by the smaller mask */            v = (((v | m0) + 1) & ~m0) | (v & m0);            /* Continue while bits covered by mask difference is non-zero */        } while (v & (m0 ^ m1));    }    /* Set unmasked bits so incrementing the reversed cursor     * operates on the masked bits of the smaller table */    v |= ~m0;    /* Increment the reverse cursor */    v = rev(v);    v++;    v = rev(v);    return v;}

Redis的dict结构中,有一个遍历dict的函数,因为该遍历函数的算法比较特别,值得单独拿出来研究一下。

首先考虑最简单的情况, 如果一个dict是稳定,即没有扩大缩小,也没有正好处于rehash的过程中,那么这种情况下的遍历是最简单的, 只需要按照索引值顺序遍历第一个hash表ht[0]就行。

但是如果本次索引的遍历的时候,跟上一个索引遍历的时候,dict已经经过了扩大或者缩小,或者dict正好处于rehash的过程中的时候,遍历过程就变复杂了。

如果仍旧采用顺序遍历索引的策略,考虑dict经过缩小的情况。如果hash表一共有8个slot,经过扩大变成了4个slot,在计算索引的时候,都是hash值同mask=size-1做与操作。所以如果某个key的hash值最后原来是7的话,在slot=8的时候,应该放到索引为7的slot,但是缩小到4个slot的时候,将被放在slot=3。可以想见,这种情况下,会有大量的key被漏掉。同样,如果经过了扩大,也会有大量的key被重复遍历。

dict的反向二进制位遍历

Redis的遍历方式采用了反向的二进制位遍历。那么什么是反向二进制位遍历呢。按照正常的遍历逻辑,遍历的顺序是按照0->1->2->3->4….

但是dictScan采用的顺序,以8个slot为例,是0->4->2->6->1->5->3->7。看着貌似没有规律,但是其实是从高位开始,向低位进位的一种遍历方式,用二进制更能直观的看出来


000 --> 100 --> 010 --> 110 --> 001 --> 101 --> 011 --> 111 --> 000

那么用这种方式遍历会有什么好处呢,可以按照dict不同状态的示例来说明

1、当dict稳定的时候

这种情况下,从上面的说明能看出,反向二进制遍历同正向二进制遍历一样,能够遍历到所有的索引,并且不会有重复或遗漏

2、当dict扩大的时候

假设dict的slot由8个扩大到16个,首先列出两种状态下的遍历顺序:

000 --> 100 --> 010 --> 110 --> 001 --> 101 --> 011 --> 111 --> 000     0000 --> 1000 --> 0100 --> 1100 --> 0010 --> 1010 --> 0110 --> 1110 --> 0001 --> 1001 --> 0101 --> 1101 --> 0011 --> 1011 --> 0111 --> 1111 --> 0000    

假设在slot=8的时候已经遍历完了010的索引,下一次就需要遍历110的索引。此时dict发生了rehash操作,slot从8个扩展到了16个,

在slot=16的情况下,就需要遍历0110的索引。 此时,在slot=8的时候遍历过的所以值为000,100,010,对应到slot=16的情况下,分别是0000,1000, 0100,1100, 0010, 1010, 正好是所以址0110的前序所以, 这样的话,整个遍历流程,不会造成遍历的重复或者遗漏。

3、当dict缩小的时候

假设在slot=16的是,已经遍历完了0110的索引,下一个就要遍历1110的索引,此时dict缩小到了slot=8 。此前已经遍历过的所有索引分别为 0000, 1000, 0100, 1100, 0010, 1010, 0110, 对应到slot=8的情况下,分别落到索引000,100,010,110。

此时需要从110的所以开始遍历,但是由于此前在slot=16的时候已经遍历过的0110的索引值也是落到110的索引,所以会造成一部分的key被重复遍历。

原哈希表长度为x,缩小后长度为y,则最多会有x/y – 1个原bucket的节点会被重复迭代。比如由16缩小为8,则最多就有1个bucket节点会重复迭代,要是由32缩小为8,则最多会有3个。

但是,也有可能,正好不重复遍历,比如slot=16的时候遍历到1010,即将遍历0110的时候dict缩小的时候,就不会产生重复。

反向二进制算法保证了dict不会遗漏元素,同时在dict缩小的时候,保证了比较小的元素重复。

4、正好进行rehash的时候

当dict正好在进行rehash的时候,当前遍历的索引里边可能数据不全,因为有一部分已经rehash到新表中去了。所以为了不漏掉元素,采取的措施是同时遍历两个hash表的对应索引。

但是rehash的过程中,不管扩大还是缩小,两张表的索引都是不同的,如何对应呢。 方法就是先比较两张hash表的大小,先遍历较小的hash表,遍历完之后,找到大表中所有对应的索引值,全部依次遍历。

举个例子,暂且不管dict当前是在扩大还是缩小,两张hash表的大小肯定是不同的。假设一张表的slot=8,另一张表的slot=32, 两个mask分别为111和11111。 假设当前的hash值同111做与操作之后为010,那么小表中010的索引遍历之后,需要遍历大表的00010,01010, 10010和11010的索引,然后将两者的索引一块返回。

v = (((v | m0) + 1) & ~m0) | (v & m0);

那么如何保证大表对应的所有索引都能遍历到呢。代码中可以看到这么一条语句,分析一下。 v | m0 将v的低位全部置为1, (v | m0) + 1则是将v的高位加1,之后再 & ~m0 将所有低位置0, v & m0 就是将v的低位提取出来放到之前的数值之后。 这样就能够: 保持v的低位不变,高位持续加1,遍历所有扩展后的slot

那么高位什么时候停止加1呢,就是while终止的条件

while (v & (m0 ^ m1))

由于m0和m1都是全为1的mask,假设m0=111, m1=11111, 那么m0 ^ m1 = 11000, 所以我们能看到当v的高位没有1了,其实就是说到头了,循环就会终止。

总结

Redis的dictScan通过反向二进制位的遍历顺序,既能防止漏掉遍历元素,也能在必须要重复遍历元素的时候,减少重复元素的个数。 同时在处理rehash操作时的遍历的时候,通过各种位操作的结合,使得rehash过程中不会漏掉元素。

原创粉丝点击