Redis Scan算法设计思想

网图侵删.jpg

想要返回redis当前数据库中的全部key应该怎么办?用keys命令?在key很是多的状况下,该命令会致使单线程redis服务器执行时间过长,后续命令得不到响应,同时对内存也会形成必定的压力,严重下降redis服务的可用性html

为此redis 2.8.0及以上版本提供了多个scan相关命令,用以针对不一样数据结构(如数据库、集合、哈希、有序集合)提供相关遍历功能c++

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements)redis

  • SCAN 命令用于迭代当前数据库中的数据库键
  • SSCAN 命令用于迭代集合键中的元素
  • HSCAN 命令用于迭代哈希键中的键值对
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)

SCAN

  • 命令格式:SCAN cursor [MATCH pattern] [COUNT count]
  • SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用以后, 都会向用户返回一个新的游标, 用户在下次迭代时须要使用这个新游标做为 SCAN 命令的游标参数, 以此来延续以前的迭代过程
  • 当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束
  • SSCAN / HSCAN /ZSCAN 与SCAN命令除命令格式有细微不一样以及在非哈希表实现下的遍历方式不一样外,其余均相似,再也不赘述,具体请点击连接查询
  • 保证:从完整遍历开始直到完整遍历结束期间, 一直存在于数据集内的全部元素都会被完整遍历返回
  • 缺点: 1)同一个元素可能会被返回屡次,在rehash 缩小后遍历或者rehash缩小过程当中遍历可能发生此状况(我的理解) 2)若是一个元素是在迭代过程当中被添加到数据集的, 又或者是在迭代过程当中从数据集中被删除的, 那么这个元素可能会被返回, 也可能不会, 这是不肯定的

注:以上内容摘自http://redisdoc.com/key/scan.html算法


对于SCAN命令和底层采用了哈希表实现的集合、哈希、有序集合,遍历时采用了一样的scan算法(都会调用dictScan函数),dictScan函数短小精悍,正是本文尝试解释的核心,以下数据库

unsigned long dictScan(dict *d,//待遍历哈希表 unsigned long v,//cursor值,这次遍历位置,初始为0 dictScanFunction *fn,//单个条目遍历函数,根据条目类型,copy条目对象,以便加入到返回对象中 dictScanBucketFunction* bucketfn,//null void *privdata)//返回对象 {
    dictht *t0, *t1;
    const dictEntry *de, *next;
    unsigned long m0, m1;

    if (dictSize(d) == 0) return 0;//若是dict为空,直接返回

    if (!dictIsRehashing(d)) {//若是此刻哈希表没有在rehashing,只有ht[0]有数据
        t0 = &(d->ht[0]);//将ht[0]做为遍历表
        m0 = t0->sizemask;//遍历表的sizemask,即以遍历表的size为底取模,如表大小为8,则m0为111

        /* 遍历cursor所在位置的全部条目 */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);//这行if条件为false,不会执行
        de = t0->table[v & m0];
        while (de) {//遍历当前cursor位置的全部条目,即hash key取模hash table大小相同的全部条目
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* 做用是将v也就是cursor的高位置为1,低位不变,如v为001,则改成61个1再加001 */
        v |= ~m0;

        /* 将cursor高位0变成1或者(连续高位1都变成0且第一个0变为1) */
        v = rev(v);//将cursor作二进制逆序,也就是变成100+61个1
        v++;//末位加1,也就是101+61个0
        v = rev(v);//将cursor作二进制逆序,也就是61个0+101

    } else {//哈希表正在rehashing
        t0 = &d->ht[0];
        t1 = &d->ht[1];

        /* 确保t0小t1大 */
        if (t0->size > t1->size) {
            t0 = &d->ht[1];
            t1 = &d->ht[0];
        }

        m0 = t0->sizemask;//t0表sizemask,如表大小为8,则m0为7,即0111
        m1 = t1->sizemask;//t1表sizemask,如表大小为64,则m1为63,即00111111

        /* 将cursor位置的全部条目都添加进去 */
        if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);//不执行
        de = t0->table[v & m0];
        while (de) {//将t0的全部条目都加进去
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* 遍历小表cursor位置可能会rehash到大表的全部条目, *如cursor为1,小表大小为8,大表大小为64,则0、八、1六、2四、3二、40、4八、56等位置的条目都会被添加返回 */
        do {
            /* 添加大表v位置的全部元素,注意v位置跟着while循环不断变化 */
            if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);//不执行
            de = t1->table[v & m1];
            while (de) {//添加v位置的全部条目
                next = de->next;
                fn(privdata, de);
                de = next;
            }

            /* 做用同上,只不过换成了大表的元素,也就是小表cursor位置可能扩展到大表的全部位置*/
            v |= ~m1;
            v = rev(v);
            v++;
            v = rev(v);

            /* 如上举例,m0为3位1,m1为6位1,两者作异或,也就是将两者不一样的高位置为1, *其余先后的61位均为0,而后遍历v在两者不一样高位的全部可能, *当v从新回到0时,跳出while循环 ,也就是将m0可能rehash到的m1位置的条目所有返回 */
        } while (v & (m0 ^ m1));
    }

    return v;
}
复制代码

这个算法很是精妙,看了挺久才明白点意思,若有不当之处,欢迎拍砖服务器

哈希表有多种状态,遍历时有可能处于微信

  • 哈希扩展后
  • 收缩后
  • 正在rehashing(扩展or收缩)中

这就使得scan算法面临的状况很复杂,怎样遍历完全部元素(遍历过程当中没有发生变化的元素保证遍历完)且尽量少的返回重复元素是个难题 三种状态具体的遍历流程图示推演发个传送门:Redis Scan迭代器遍历操做原理(二)–dictScan反向二进制迭代器 (网上搜的,流程很长,慎点,可是有一些不错的图)数据结构

具体算法流程也可参见上面的源码注释函数


算法思想(把收缩当作反向的扩张)

一、假设hash表大小从N扩张为2^M x N(哈希表大小只可能为2的幂数,N也为2的幂数),那么原先hash表的i元素可能被分布到i + j x N where j <- [0, 2^M-1]位置,如N为4,M为3,则i(原先为1)可能被分散到一、1+1x四、1+2x4...、1+7x4位置,注意这些位置,它们后面的log(N)位是相同的,也就是前面的M位不一样,如spa

  • 00001
  • 00101
  • 01001
  • ...
  • 11101

若是在扩展后遍历的过程当中能将后面两位相同都为01的位置都忽略,也就是只要后面N位相同的遍历完了,意味着前面M位的全部可能性也都列举完了,即老是先把前面的可能性穷举完,再穷举后面的位,那么扩展后的slot(如1对应的一、五、9...、29)就没必要从新再从新遍历一遍了,收缩是相似的,只不过收缩后的位置可能包含原哈希表高位还没有穷举完的可能性,须要再次遍历

二、怎么先遍历高位的可能性,dictScan给出了反向二进制迭代算法(老是先将最高位置取反,穷举高位的可能性,依次向低位推动,这种变换方式确保了全部元素都会被遍历到):

  • 将第一个遇到的高位0对应的位置置1(即变换先后两者拥有最多的从右向左连续相同低位,也就是模相同的范围最大),在该规则下,32大小哈希表,00001遍历后的下一个位置是10001,若是下次遍历10001时哈希表收缩成16大小,则会从新遍历0001位置(10001与16取模),00001和10001都收缩到了该位置,这种状况下元素可能重复返回;若是32扩展为64,则00001扩张为000001/100001两个位置,因为高位穷举的原则,则后续这些位置不会再次处理,下降了元素重复返回的几率
  • 或将前面的连续1置为0,第一个0置为1,如10001下一个是01001,即开始穷举下一个高位的可能性

三、rehashing这种状况,须要在遍历完小表cursor位置后将小表cursor位置可能rehash到的大表全部位置所有遍历一遍,而后再返回遍历元素和下一小表遍历位置

欢迎关注个人微信公众号

68号小喇叭
相关文章
相关标签/搜索