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號小喇叭
相關文章
相關標籤/搜索