【Redis5源碼學習】淺析redis命令之scan篇

Grapephp


命令語法

命令含義:

增量迭代一個集合元素。node

命令格式:

SCAN cursor [MATCH pattern] [COUNT count]

命令實戰:

基本的執行遍歷
redis 127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"
COUNT選項

對於增量式迭代命令不保證每次迭代所返回的元素數量,咱們可使用COUNT選項, 對命令的行爲進行必定程度上的調整。COUNT 選項的做用就是讓用戶告知迭代命令, 在每次迭代中應該從數據集裏返回多少元素。使用COUNT 選項對於對增量式迭代命令至關於一種提示, 大多數狀況下這種提示都比較有效的控制了返回值的數量。git

COUNT 參數的默認值爲 10 。
數據集比較大時,若是沒有使用MATCH 選項, 那麼命令返回的元素數量一般和 COUNT 選項指定的同樣, 或者比 COUNT 選項指定的數量稍多一些。
在迭代一個編碼爲整數集合(intset,一個只由整數值構成的小集合)、 或者編碼爲壓縮列表(ziplist,由不一樣值構成的一個小哈希或者一個小有序集合)時, 增量式迭代命令一般會無視 COUNT 選項指定的值, 在第一次迭代就將數據集包含的全部元素都返回給用戶。
注意: 並不是每次迭代都要使用相同的 COUNT 值 ,用戶能夠在每次迭代中按本身的須要隨意改變 COUNT 值, 只要記得將上次迭代返回的遊標用到下次迭代裏面就能夠了。github

127.0.0.1:6379> scan 0 count 2
1) "12"
2) 1) "user_level_1"
   2) "mykey"
127.0.0.1:6379>
MATCH 選項

相似於KEYS 命令,增量式迭代命令經過給定 MATCH 參數的方式實現了經過提供一個 glob 風格的模式參數, 讓命令只返回和給定模式相匹配的元素。redis

redis 127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood
(integer) 6
redis 127.0.0.1:6379> sscan myset 0 match f*
1) "0"
2) 1) "foo"
   2) "feelsgood"
   3) "foobar"
redis 127.0.0.1:6379>

返回值:

SCAN, SSCAN, HSCAN 和 ZSCAN 命令都返回一個包含兩個元素的 multi-bulk
回覆: 回覆的第一個元素是字符串表示的無符號 64 位整數(遊標),回覆的第二個元素是另外一個 multi-bulk 回覆, 包含了本次被迭代的元素。segmentfault

源碼分析

此篇以scan命令爲例。數組

命令入口

/* The SCAN command completely relies on scanGenericCommand. */
void scanCommand(client *c) {
    unsigned long cursor;
    if (parseScanCursorOrReply(c,c->argv[1],&cursor) == C_ERR) return;
    scanGenericCommand(c,NULL,cursor);
}

處理遊標

/* 嘗試解析存儲在對象「o」中的掃描遊標:若是遊標有效,
 * 則將其做爲無符號整數存儲到*cursor中,並返回C_OK。不然返回C_ERR並向客戶機發送錯誤。
 * 此處o->ptr存儲咱們輸入的遊標
*/
int parseScanCursorOrReply(client *c, robj *o, unsigned long *cursor) {
    char *eptr;
    /* 使用strtoul(),由於咱們須要一個無符號long,
     * 因此getLongLongFromObject()不會覆蓋整個遊標空間。
     */
    errno = 0;
    *cursor = strtoul(o->ptr, &eptr, 10);
    if (isspace(((char*)o->ptr)[0]) || eptr[0] != '\0' || errno == ERANGE)
    {
        addReplyError(c, "invalid cursor");
        return C_ERR;
    }
    return C_OK;
}

scan的公用函數

void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
    int i, j;
    list *keys = listCreate();
    listNode *node, *nextnode;
    long count = 10;
    sds pat = NULL;
    int patlen = 0, use_pattern = 0;
    dict *ht;
    /* 對象必須爲空(以迭代鍵名),或者對象的類型必須設置爲集合,排序集合或散列。*/
    serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
                o->type == OBJ_ZSET);
    /* 將i設置爲第一個選項參數。前一個是遊標。在對象爲空時第一個參數在第2個位置,不然爲第三個位置,例如:scan 0 ,sscan myset 0 match f*; */
    i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */

scan的實際操做一共分爲4步,下邊咱們來看下這四步。網絡

step1:解析命令選項

/* Step 1:解析選項. */
    while (i < c->argc) {
        j = c->argc - i;
        // count選項,注意是從第二個開始
        if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
            if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL) //獲取所傳遞的值count值並賦值給count,由於在count關鍵字後邊是count的值,因此爲c->argv[i+1].
                != C_OK)
            {
                goto cleanup; //清理list等
            }
            //若是count的值爲1,返回錯誤。清空在函數開頭建立的list。
            if (count < 1) {
                addReply(c,shared.syntaxerr);
                goto cleanup;
            }
            i += 2;
        } else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
        // match選項,一樣是從第二個開始
            pat = c->argv[i+1]->ptr;   //獲取到匹配規則
            patlen = sdslen(pat);
            /* 若是模式徹底是「*」,那麼它老是匹配的,因此這至關於禁用它。也就是說這種狀況下此模式無關緊要 */
            use_pattern = !(pat[0] == '*' && patlen == 1);
            i += 2;
        } else {
            addReply(c,shared.syntaxerr);
            goto cleanup;
        }
    }

此步驟主要是對命令的解析,解析出count和match的值以及對相應變量的賦值,從而在下文過濾步驟中進行處理。數據結構

step2:遍歷集合構造list

/* Step 2: 遍歷集合。 
     *
     *請注意,若是對象是用ziplist、intset或任何其餘非哈希表的表示進行編碼的,則能夠確定它也是由少許元素組成的。所以,爲了不獲取狀態,咱們只需在一次調用中返回對象內部的全部內容,將遊標設置爲0表示迭代結束。 */
    /* 處理哈希表的狀況. 對應o的不一樣類型*/
    ht = NULL;
    if (o == NULL) {
        ht = c->db->dict;
    } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
        ht = o->ptr;
    } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
        ht = o->ptr;
        count *= 2; /* We return key / value for this type. */
    } else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
        zset *zs = o->ptr;
        ht = zs->dict;
        count *= 2; /* We return key / value for this type. */
    }
    if (ht) {     //通常的存儲,不是intset, ziplist
        void *privdata[2];
        /*咱們將迭代的最大次數設置爲指定計數的10倍,所以若是哈希表處於病態狀態(很是稀疏地填充),
          咱們將避免以返回沒有或不多元素爲代價來阻塞太多時間。 */
        long maxiterations = count*10;
        /* 咱們向回調傳遞兩個指針:一個是它將向其中添加新元素的列表,
           另外一個是包含dictionary的對象,以便可以以類型相關的方式獲取更多數據。 */
        privdata[0] = keys;
        privdata[1] = o;
        do {
            //一個個掃描,從cursor開始,而後調用回調函數將數據設置到keys返回數據集裏面。
            cursor = dictScan(ht, cursor, scanCallback, NULL, privdata);
        } while (cursor &&
              maxiterations-- &&
              listLength(keys) < (unsigned long)count);
    } else if (o->type == OBJ_SET) {  //若是是set,將這個set裏面的數據所有返回,由於它是壓縮的intset,會很小的。
        int pos = 0;
        int64_t ll;
        while(intsetGet(o->ptr,pos++,&ll))
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {
        //ziplist或者hash,字符串表示的數據結構,不會太大。
        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 {
        serverPanic("Not handled encoding in SCAN.");
    }

此步驟根據不一樣的格式作出不一樣的處理,將掃描出來的元素放在list集合中,以方便過濾與取數。函數

step3:過濾元素

/* Step 3: 過濾元素.此處是遍歷上文構造的list */
    node = listFirst(keys);
    while (node) {
        robj *kobj = listNodeValue(node);
        nextnode = listNextNode(node);
        int filter = 0;
        /* 若是它不匹配的模式則過濾,此處的過濾是在上文給出. */
        if (!filter && use_pattern) {
            if (sdsEncodedObject(kobj)) {
                if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
                    filter = 1;
            } else {
                char buf[LONG_STR_SIZE];
                int len;
                serverAssert(kobj->encoding == OBJ_ENCODING_INT);
                len = ll2string(buf,sizeof(buf),(long)kobj->ptr);
                if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1;
            }
        }
        /* 若是key過時,過濾. */
        if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;
        /* 若是須要過濾,刪除元素及其已設置的值. */
        if (filter) {
            decrRefCount(kobj);
            listDelNode(keys, node);
        }
       
        /* 若是這是一個散列或排序集,咱們有一個鍵-值元素的平面列表,所以若是這個元素被過濾了,
           那麼刪除這個值,或者若是它沒有被過濾,那麼跳過它:咱們只匹配鍵。*/
        if (o && (o->type == OBJ_ZSET || o->type == OBJ_HASH)) {
            node = nextnode;
            nextnode = listNextNode(node);
            if (filter) {
                kobj = listNodeValue(node);
                decrRefCount(kobj);
                listDelNode(keys, node);
            }
        }
        node = nextnode;
    }

根據match參數過濾返回值,而且若是這個鍵已通過期也會直接過濾掉。最後返回元素。

step4:返回消息給客戶端+清理

/* Step 4: 返回消息給客戶端. */
    addReplyMultiBulkLen(c, 2);
    addReplyBulkLongLong(c,cursor);
    addReplyMultiBulkLen(c, listLength(keys));
    while ((node = listFirst(keys)) != NULL) {
        robj *kobj = listNodeValue(node);
        addReplyBulk(c, kobj);
        decrRefCount(kobj);
        listDelNode(keys, node);
    }
//清理操做,清楚list等結構
cleanup:
    listSetFreeMethod(keys,decrRefCountVoid);
    listRelease(keys);
}

綜上所述,scan能夠分爲四步:

  • 解析count和match參數.若是沒有指定count,默認返回10條數據
  • 開始迭代集合,若是是key保存爲ziplist或者intset,則一次性返回全部數據,沒有遊標(遊標值直接返回0).因爲redis設計只有數據量比較小的時候纔會保存爲ziplist或者intset,因此此處不會影響性能.
  • 遊標在保存爲hash的時候發揮做用,具體入口函數爲dictScan,詳情可見dictScan原理
  • 根據match參數過濾返回值,而且若是這個鍵已通過期也會直接過濾掉(redis中鍵過時以後並不會當即刪除,此處涉及到redis的兩種過時刪除機制,惰性刪除和按期刪除)
  • 返回結果到客戶端,是一個數組,第一個值是遊標,第二個值是具體的鍵值對

擴展

查找大key的方法:

bigkeys參數

redis-cli 提供一個bigkeys參數,能夠掃描redis中的大key

執行結果:

root@grape ~]# redis-cli --bigkeys
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far 'testLuaSet' with 11 bytes
[00.00%] Biggest string found so far 'number' with 18 bytes
-------- summary -------
Sampled 2 keys in the keyspace!
Total key length in bytes is 16 (avg len 8.00)
Biggest string found 'number' has 18 bytes
2 strings with 29 bytes (100.00% of keys, avg size 14.50)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

此參數命令比較簡單,使用scan命令去遍歷全部的鍵,對每一個鍵根據其類型執行"STRLEN","LLEN","SCARD","HLEN","ZCARD"這些命令獲取其長度或者元素個數。
另外該方法有兩個缺點:

  • 1.線上使用:雖然scan命令經過遊標遍歷建空間而且在生產上能夠經過對從服務執行該命令,但畢竟是一個線上操做
  • 2.set,zset,list以及hash類型只能獲取有多少個元素。但其實元素多的不必定佔用空間大

經過RDB文件

在redis中定義了一些opcode(1字節),去標記opcode以後保存的是什麼類型的數據,在這些類型中有一個value-type值類型,以下圖:

clipboard.png

value_type就是值類型這一列,括號中的數字就是保存到rdb文件中時的實際使用數字。
咱們能夠寫代碼解析rdb文件,經過value_type去獲取每一個value的大小。
在這裏咱們推薦一個開源軟件:godis-cli-bigkey
詳情見github:https://github.com/erpeng/god...

scan的優缺點

  • 提供鍵空間的遍歷操做,支持遊標,複雜度O(1), 總體遍歷一遍只須要O(N);
  • 提供結果模式匹配;
  • 支持一次返回的數據條數設置,但僅僅是個hints,有時候返回的會多;
  • 弱狀態,全部狀態只須要客戶端須要維護一個遊標;
  • 沒法提供完整的快照遍歷,也就是中間若是有數據修改,可能有些涉及改動的數據遍歷不到;
  • 每次返回的數據條數不必定,極度依賴內部實現;
  • 返回的數據可能有重複,應用層必須可以處理重入邏輯;
  • count是每次掃描的key個數,並非結果集個數。count要根據掃描數據量大小而定,Scan雖然無鎖,可是也不能保證在超過百萬數據量級別搜索效率;count不能過小,網絡交互會變多,count要儘量的大。在搜索結果集1萬之內,建議直接設置爲與所蒐集大小相同

參考文章

相關文章
相關標籤/搜索