• Redis源码解析:10scan类命令的实现


            像keys或者smembers命令,需要遍历数据集合中的所有元素。在一个大的数据库中使用,可能会阻塞服务器较长的一段时间,造成性能问题,因此不适用与生产环境。

            在Redis2.8.0中引入了scan类命令,可用于迭代整个数据库或者是哈希键、集合键和有序集合键这样的数据集合。

            scan类命令支持增量式迭代,它们每次执行都只会返回少量元素,所以这些命令可以用于生产环境。

            scan类命令使用游标迭代数据集合,命令每次被调用之后,都会向用户返回一个新的游标,用户在下次迭代时,使用这个新游标作为scan类命令的游标参数,以此来延续之前的迭代过程。

            第一次迭代时,游标参数置为0,经过若干次迭代之后,如果返回的游标为0,表示完成了一次完整迭代。

     

            scan类命令包括:scan命令用于迭代当前的数据库,返回其中的键;sscan命令用于迭代集合键,返回集合中的元素;hscan用于迭代哈希键,返回其中的字段以及字段值;zscan用于迭代有序集合键,返回其中的元素及其分数。

     

    一:用法

            scan类命令的用法如下:

    SCAN cursor [MATCH pattern] [COUNT count]
    HSCAN key cursor [MATCH pattern] [COUNT count]
    SSCAN key cursor [MATCH pattern] [COUNT count]
    ZSCAN key cursor [MATCH pattern] [COUNT count]

            COUNT 选项指定每次迭代返回元素的最大值,默认值为10。

            注意,对于一个使用哈希表作为底层实现的大数据集来说,scan类命令每次最多会返回count个元素;如果数据集合不是以哈希表作为底层实现的话,则scan类命令无视count选项,直接返回数据集合中的所有元素。

            并非每次迭代都要使用相同的COUNT值。

     

            MATCH选项,可以提供一个glob风格的模式参数,让scan类命令只返回和给定模式相匹配的元素。

            注意,对元素的模式匹配,是在命令从数据集中取出元素之后,向客户端返回元素之前的进行的,所以如果被迭代的数据集中只有少量元素和模式相匹配,那么迭代命令可能不返回任何元素。

     

            例子如下:

    127.0.0.1:6379> scan 0
    1) "7"
    2)  1) "msg3"
        2) "msg"
        3) "msg5"
        4) "msg6"
        5) "msg4"
        6) "msg2"
        7) "msg8"
        8) "msg11"
        9) "msg7"
       10) "msg10"
    127.0.0.1:6379> scan 7
    1) "0"
    2) 1) "msg9"
       2) "msg1"

            scan命令只能对被返回的元素提供有限的保证:

            a:从完整遍历开始直到完整遍历结束期间,一直存在于数据集内的所有元素都会被完整遍历返回;

            b:同一个元素可能会被返回多次;如果一个元素是在迭代过程中被添加到数据集的,又或者是在迭代过程中从数据集中被删除的,那么这个元素可能会被返回,也可能不会, 这是未定义的。

            c:不保证每次执行都返回某个给定数量的元素。甚至可能会返回零个元素,但只要命令返回的游标不是 0,应用程序就不应该将迭代视作结束。

     

    二:实现

             scan命令是通过src/db.c中的scanCommand函数实现的,该函数的代码如下:

    void scanCommand(redisClient *c) {
        unsigned long cursor;
        if (parseScanCursorOrReply(c,c->argv[1],&cursor) == REDIS_ERR) return;
        scanGenericCommand(c,NULL,cursor);
    }

            该函数首先通过parseScanCursorOrReply,解析客户端命令参数中的游标参数,赋值到cursor中。如果解析失败,则直接返回;解析成功后,调用scanGenericCommand进行处理。

     

            下面是解析游标参数的函数parseScanCursorOrReply的实现:

    int parseScanCursorOrReply(redisClient *c, robj *o, unsigned long *cursor) {
        char *eptr;
    
        /* Use strtoul() because we need an *unsigned* long, so
         * getLongLongFromObject() does not cover the whole cursor space. */
        errno = 0;
        *cursor = strtoul(o->ptr, &eptr, 10);
        if (isspace(((char*)o->ptr)[0]) || eptr[0] != '' || errno == ERANGE)
        {
            addReplyError(c, "invalid cursor");
            return REDIS_ERR;
        }
        return REDIS_OK;
    }

            该函数主要是调用strtoul对存放在字符串对象o中的字符串进行解析,如果该字符串是有效的数字表示,则将其转换成整数后放到参数cursor中,并返回REDIS_OK,否则反馈给客户端"invalid cursor",并返回REDIS_ERR。

     

            下面是主要的实现函数scanGenericCommand的代码,该函数不止是scan命令的主要实现函数,还是sscan、hscan和zscan命令的主要实现函数:

    void scanCallback(void *privdata, const dictEntry *de) {
        void **pd = (void**) privdata;
        list *keys = pd[0];
        robj *o = pd[1];
        robj *key, *val = NULL;
    
        if (o == NULL) {
            sds sdskey = dictGetKey(de);
            key = createStringObject(sdskey, sdslen(sdskey));
        } else if (o->type == REDIS_SET) {
            key = dictGetKey(de);
            incrRefCount(key);
        } else if (o->type == REDIS_HASH) {
            key = dictGetKey(de);
            incrRefCount(key);
            val = dictGetVal(de);
            incrRefCount(val);
        } else if (o->type == REDIS_ZSET) {
            key = dictGetKey(de);
            incrRefCount(key);
            val = createStringObjectFromLongDouble(*(double*)dictGetVal(de),0);
        } else {
            redisPanic("Type not handled in SCAN callback.");
        }
    
        listAddNodeTail(keys, key);
        if (val) listAddNodeTail(keys, val);
    }
    
    void scanGenericCommand(redisClient *c, robj *o, unsigned long cursor) {
        int i, j;
        list *keys = listCreate();
        listNode *node, *nextnode;
        long count = 10;
        sds pat;
        int patlen, use_pattern = 0;
        dict *ht;
    
        /* Object must be NULL (to iterate keys names), or the type of the object
         * must be Set, Sorted Set, or Hash. */
        redisAssert(o == NULL || o->type == REDIS_SET || o->type == REDIS_HASH ||
                    o->type == REDIS_ZSET);
    
        /* Set i to the first option argument. The previous one is the cursor. */
        i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */
    
        /* Step 1: Parse options. */
        while (i < c->argc) {
            j = c->argc - i;
            if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
                if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
                    != REDIS_OK)
                {
                    goto cleanup;
                }
    
                if (count < 1) {
                    addReply(c,shared.syntaxerr);
                    goto cleanup;
                }
    
                i += 2;
            } else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
                pat = c->argv[i+1]->ptr;
                patlen = sdslen(pat);
    
                /* The pattern always matches if it is exactly "*", so it is
                 * equivalent to disabling it. */
                use_pattern = !(pat[0] == '*' && patlen == 1);
    
                i += 2;
            } else {
                addReply(c,shared.syntaxerr);
                goto cleanup;
            }
        }
    
        /* Step 2: Iterate the collection.
         *
         * Note that if the object is encoded with a ziplist, intset, or any other
         * representation that is not a hash table, we are sure that it is also
         * composed of a small number of elements. So to avoid taking state we
         * just return everything inside the object in a single call, setting the
         * cursor to zero to signal the end of the iteration. */
    
        /* 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. */
        }
    
        if (ht) {
            void *privdata[2];
            /* We set the max number of iterations to ten times the specified
             * COUNT, so if the hash table is in a pathological state (very
             * sparsely populated) we avoid to block too much time at the cost
             * of returning no or very few elements. */
            long maxiterations = count*10;
    
            /* 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 = dictScan(ht, cursor, scanCallback, privdata);
            } while (cursor &&
                  maxiterations-- &&
                  listLength(keys) < (unsigned long)count);
        } else if (o->type == REDIS_SET) {
            int pos = 0;
            int64_t ll;
    
            while(intsetGet(o->ptr,pos++,&ll))
                listAddNodeTail(keys,createStringObjectFromLongLong(ll));
            cursor = 0;
        } else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {
            unsigned char *p = ziplistIndex(o->ptr,0);
            unsigned char *vstr;
            unsigned int vlen;
            long long vll;
    
            while(p) {
                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.");
        }
    
        /* Step 3: Filter elements. */
        node = listFirst(keys);
        while (node) {
            robj *kobj = listNodeValue(node);
            nextnode = listNextNode(node);
            int filter = 0;
    
            /* Filter element if it does not match the pattern. */
            if (!filter && use_pattern) {
                if (sdsEncodedObject(kobj)) {
                    if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
                        filter = 1;
                } else {
                    char buf[REDIS_LONGSTR_SIZE];
                    int len;
    
                    redisAssert(kobj->encoding == REDIS_ENCODING_INT);
                    len = ll2string(buf,sizeof(buf),(long)kobj->ptr);
                    if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1;
                }
            }
    
            /* Filter element if it is an expired key. */
            if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;
    
            /* Remove the element and its associted value if needed. */
            if (filter) {
                decrRefCount(kobj);
                listDelNode(keys, node);
            }
    
            /* If this is a hash or a sorted set, we have a flat list of
             * key-value elements, so if this element was filtered, remove the
             * value, or skip it if it was not filtered: we only match keys. */
            if (o && (o->type == REDIS_ZSET || o->type == REDIS_HASH)) {
                node = nextnode;
                nextnode = listNextNode(node);
                if (filter) {
                    kobj = listNodeValue(node);
                    decrRefCount(kobj);
                    listDelNode(keys, node);
                }
            }
            node = nextnode;
        }
    
        /* Step 4: Reply to the client. */
        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);
        }
    
    cleanup:
        listSetFreeMethod(keys,decrRefCountVoid);
        listRelease(keys);
    }

            在函数scanGenericCommand中,参数o表示要迭代的数据集合,首先对o进行校验,如果o不是NULL,或者o的类型不是REDIS_SET、REDIS_HASH、REDIS_ZSET其中之一的话,则直接报错退出;

     

            然后,将i置为命令中第一个可选参数的索引;对于scan命令,i为2,其他scan命令,i为3;接下来开始解析可选参数count和match的值,如果没有可选参数count,则将其设置为10;如果出现解析错误的情况,则向客户端反馈syntaxerr,并在清理工作之后返回;

            

            接下来开始遍历数据。如果要遍历的对象,不是通过哈希表实现的,比如哈希对象和有序集合可以通过压缩表实现,集合对象可以通过intset实现,这种情况下,则无视count值,直接返回对象中的所有元素;另外,对于使用字典实现哈希对象,以及跳跃表实现的有序集合来说,需要返回字典中的键值对,因此需要将count*=2;

            如果对象是通过字典实现的,则通过dictScan对字典进行遍历,针对字典对象的每一个键值对,调用scanCallback,该函数针对不同的对象类型,从字典中取出相应的值,放入到链表keys中。这种情况下,最多调用count次dictScan;另外,为了避免字典处于罕见的错误状态(字典在rehash时的某种状态,这种状态下dictScan可能不返回或者返回极少的元素),最多调用10*count次dictScan;

            如果对象不是通过字典实现,并且是集合对象的话,则集合对象是通过intset实现的,此时调用intsetGet,将集合中的所有元素都遍历一遍,插入到链表keys中,并置cursor为0;

            如果对象不是通过字典实现,并且是哈希对象或者有序集合对象的话,则它们是通过压缩表实现的,此时调用ziplistGet,将压缩列表中的所有元素都遍历一遍,插入到链表keys中,并置cursor为0;

            

            接下来,开始对keys中的数据进行过滤,针对keys中的每一个元素,如果命令参数中指定了过滤选项的话,如果该元素是字符串的话,则直接使用stringmatchlen进行匹配,如果元素是以整数进行存储的话,则先将其转换成字符串,再用stringmatchlen进行匹配。匹配不成功,则置filter为1,表示需要舍弃该元素;

            如果是遍历数据库的话,还需要用expireIfNeeded判断该键是否因超时需要删除,如果是,则也置filter为1;

            如果filter为1,则将该元素从keys中删除;

            如果对象为哈希对象或者有序集合的话,则keys中不但保存了其中的key,还保存了对应的键,所以下一次过滤操作需要隔一个元素开始,而且,如果当前元素被舍弃掉的话,还需要删除该元素的后继元素(键元素对应的值);

     

            过滤完成后,将cursor和keys中的所有元素都反馈给客户端;最后做清理工作。

     

            scan的详细解释可以参考:

    http://redis.io/commands/scan,它的中文翻译见:http://doc.redisfans.com/key/scan.html

     

     

     

  • 相关阅读:
    priority of uncertainty
    0523
    6.  Design Partition Guidelines for ThirdParty IP Delivery
    0604
    the categories of constraints
    priority of setup/hold
    SECTION 4: THE TIMEQUEST GUI
    tiny mistake made confusing issues
    fgetcsv 读取csv文件出现问题解决办法
    安卓笔记之配置第一个程序
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247065.html
Copyright © 2020-2023  润新知