Redis Scan迭代器遍歷操作原理(一)


Redis在2.8.0版本新增了眾望所歸的scan操作,從此再也不用擔心敲入了keys*, 然后舉起雙手看着鍵盤等待漫長的系統卡死了···

命令的官方介紹在這里, 中文版由huangz同學細心翻譯了,作者Antirez的介紹在這里:Finally Redis collections are iterable (我又邪惡的想到了之前他那次機器down機的事故了···)。

具體的使用參考上面的鏈接即可,這里大概介紹一下Scan操作的實現原理。

Redis的SCAN操作由於其整體的數據設計,無法提供特別准的scan操作,僅僅是一個“can ‘ t guarantee , just do my best”的實現,優缺點如下:

  • 優點:
    • 提供鍵空間的遍歷操作,支持游標,復雜度O(1), 整體遍歷一遍只需要O(N);
    • 提供結果模式匹配;
    • 支持一次返回的數據條數設置,但僅僅是個hints,有時候返回的會多;
    • 弱狀態,所有狀態只需要客戶端需要維護一個游標;
  • 缺點:
    • 無法提供完整的快照遍歷,也就是中間如果有數據修改,可能有些涉及改動的數據遍歷不到;
    • 每次返回的數據條數不一定,極度依賴內部實現;
    • 返回的數據可能有重復,應用層必須能夠處理重入邏輯;

所以結論是Scan是一個不錯的但也讓人又愛又恨的命令···。下面來介紹一下代碼。

首先scanCommand 函數處理簡單的scan操作,其他類似hscan函數跟這個的區別就是hscan需要取獲取一遍key對應的空間或者說域,他們主要都是嚼用了通用的scan操作函數:scanGenericCommand 。

scanGenericCommand 函數分4步:

第一步當然就是解析參數了,比如count, match匹配參數;

第二部是需要去做真正的掃描鍵 的操作了,redis為了性能考慮,對於小數據結構會轉換為ziplist,intset數據結構因此需要區分這2類,對於后者,由於其本身比較小,因此可完全可以在這一次scan操作的時候返還所有的數據,反正不大的。

另外一類就是正常的hash表所代表的掃描了,其掃描路徑比較復雜,好吧,我看了好幾次都沒有看明白這到底是怎么掃描的,這幾天啃也要啃出來!

    /* Handle the case of a hash table. */
    ht = NULL;
    if (o == NULL) {//鍵掃描
        ht = c->db->dict;
    } else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
    } else if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_HT) {
        ht = o->ptr;
        count *= 2; /* We return key / value for this type. */
    } else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = o->ptr;
        ht = zs->dict;
        count *= 2; /* We return key / value for this type. */
    }
//由於redis的ziplist, intset等類型數據量挺少,所以可用一次返回的。下面的else if 做這個事情。全部返回一個key 。
    if (ht) {//一般的存儲,不是intset, ziplist
        void *privdata[2];

        /* We pass two pointers to the callback: the list to which it will
         * add new elements, and the object containing the dictionary so that
         * it is possible to fetch more data in a type-dependent way. */
        privdata[0] = keys;
        privdata[1] = o;
        do {
        	//一個個掃描,從cursor開始,然后調用回調函數將數據設置到keys返回數據集里面。
            cursor = dictScan(ht, cursor, scanCallback, privdata);
        } while (cursor && listLength(keys) < count);     } else if (o->type == REDIS_SET) {
        int pos = 0;
        int64_t ll;

        while(intsetGet(o->ptr,pos++,&ll))//將這個set里面的數據全部返回,因為它是壓縮的intset,會很小的。
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {//那么一定是ziplist了,字符串表示的數據結構,不會太大。
        unsigned char *p = ziplistIndex(o->ptr,0);
        unsigned char *vstr;
        unsigned int vlen;
        long long vll;

        while(p) {//掃描整個鍵,然后全部返回這一條。並且返回cursor為0表示沒東西了。其實這個就等於沒有遍歷
            ziplistGet(p,&vstr,&vlen,&vll);
            listAddNodeTail(keys,
                 (vstr != NULL) ? createStringObject((char*)vstr,vlen) : createStringObjectFromLongLong(vll));
            p = ziplistNext(o->ptr,p);
        }
        cursor = 0;
    } else {
        redisPanic("Not handled encoding in SCAN.");
    }

上面簡單的地方在於如果這個鍵是已REDIS_SET或者REDIS_HASH或者REDIS_ZSET行事存儲的話,那么只需要掃描所有的鍵,然后一個個將其加入到臨時的列表里面,以備返回給客戶端。

最難的地方在於dictScan 函數,里面是各種位運算。

隨后第三步就是進行結果的過濾了,一般就是用match參數代表的字符串去做匹配,看是否需要過濾數據。

第四步就是將收集到的數據返回給客戶端。然后就完成了請求。

dictScan  原理:

好吧,我看了2次,沒看懂·····先做飯··

ps: 寫着寫着發現一篇文章寫不完,所以令起一篇了:Redis Scan迭代器遍歷操作原理(二)–dictScan反向二進制迭代器  , 希望能講清楚.

Share
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM