redis的scan命令的源码分析,实现原理

简言

1. 线上环境keys命令不可用,会导致redis卡死。scan命令因为可以分批遍历,比较实用

2. scan命令包括多个

    遍历整个数据库的scan命令,处理函数 scanCommand(),最终调用scanGenericCommand()

    遍历hash对象的hscan命令,处理函数 hscanCommand(),最终调用scanGenericCommand()

    遍历set对象的sscan命令,处理函数 sscanCommand(),最终调用scanGenericCommand()

    遍历zset对象的zscan命令,处理函数 zscanCommand(),最终调用scanGenericCommand()

3. 命令格式(0,1,2,3,4,5用来表下标,代码中会根据下标解析参数match,count等)

 0      1      2      3       4     5   
SCAN cursor [MATCH pattern] [COUNT count]
 0     1    2       3      4        5     6
SSCAN KEY cursor [MATCH pattern] [COUNT count]
HSCAN KEY cursor [MATCH pattern] [COUNT count]
ZSCAN KEY cursor [MATCH pattern] [COUNT count]

所以我们重点分析函数 scanGenericCommand(),笔者redis版本是6.0.0

// 这个函数用来实现 SCAN,HSCAN,SSCAN,ZSCAN命令
// 如果参数 o 不为NULL,说明对象一定是hash, set 或者 zset;如果参数 o 为NULL,说明是遍历当前整个数据库
void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
    int i, j;

    // 新建一个list,用来暂存一定量的未筛选的key
    list *keys = listCreate();
    listNode *node, *nextnode;
    // 如果客户端不传count值,那么默认是10
    long count = 10;

    // pat 表 要match的字符串
    sds pat = NULL;
    sds typename = NULL;

    //  patlen 表 match字符串的长度,use_pattern 表是否使用是通配符*,1表是*
    int patlen = 0, use_pattern = 0;
    dict *ht;

    // Assert断言,o只能是NULL, set对象,hash对象,zset对象之一
    serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
                o->type == OBJ_ZSET);

    // i用来指向第一个参数下标,o为NULL说明是遍历当前整个库,不需要传key,所以跳过
    i = (o == NULL) ? 2 : 3; 

    // 第一步:解析参数
    while (i < c->argc) {
        // j表剩余参数个数,j一定要是偶数,因为参数名字,参数值是成对的
        j = c->argc - i;
        
        // 解析参数 count,不区分大小写
        if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
            // i指向count,那么i+1指向count的值,做解析后保存在count变量中
            if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
                != C_OK)
            {
                goto cleanup;
            }

            // count小于1则报错
            if (count < 1) {
                addReply(c,shared.syntaxerr);
                goto cleanup;
            }

            // 因为参数是成对的,所以跳2
            i += 2;
        } 
        // 解析参数 match,不区分大小写
        else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
            pat = c->argv[i+1]->ptr;
            patlen = sdslen(pat);

            // 若是*,那么use_pattern置为1
            use_pattern = !(pat[0] == '*' && patlen == 1);

            // 因为参数是成对的,所以跳2
            i += 2;
        } 
        // 解析参数 type, 不区分大小写
        else if (!strcasecmp(c->argv[i]->ptr, "type") && o == NULL && j >= 2) {
            /* SCAN for a particular type only applies to the db dict */
            typename = c->argv[i+1]->ptr;
            i+= 2;
        } 
        // 都不是,说明参数传错了,直接返回错误
        else {
            addReply(c,shared.syntaxerr);
            goto cleanup;
        }
    }

    // 第二步:遍历集合
    ht = NULL;

    // 如果参数 o  
    // 为 NULL
    // 为 set  且内部编码为 OBJ_ENCODING_HT;
    // 为 hash 且内部编码为 OBJ_ENCODING_HT;
    // 为 zset 且内部编码为 OBJ_ENCODING_SKIPLIST;
    // 这三种情况下,说明元素个数很多,遍历时为防止redis卡顿,需要增量遍历,此时让count起效,给ht赋值
    // 其他情况,比如用ziplist(压缩列表)实现的对象,说明元素个数很少,直接全部遍历就行了,此时count不起效
    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) {
        void *privdata[2];

        // 最多遍历次数,防止过多占用cpu
        long maxiterations = count*10;
        
        // 传入的参数:存储key的列表,字典对象
        privdata[0] = keys;
        privdata[1] = o;
        
        // 遍历,每次取出一部分key放进keys中,并更新cursor,再每次判断keys的个数,需不能超过count限定
        // 由此可见其实返回的个数是有可能超过count的
        do {
            cursor = dictScan(ht, cursor, scanCallback, NULL, privdata);
        } while (cursor &&
              maxiterations-- &&
              listLength(keys) < (unsigned long)count);

        // 注意此时cursor并不一定是0
    } 
    // set 对象,且是压缩列表实现的,全遍历,此时count不起效
    else if (o->type == OBJ_SET) 
    {
        int pos = 0;
        int64_t ll;

        while(intsetGet(o->ptr,pos++,&ll))
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));

        // 全遍历,则 cursor 置为0
        cursor = 0;
    } 
    // has,zset 对象,且是压缩列表实现的,全遍历,此时count不起效
    else if (o->type == OBJ_HASH || o->type == OBJ_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
        cursor = 0;
    } else {
        serverPanic("Not handled encoding in SCAN.");
    }

    // 第三步:筛选keys,很简单,keys是个列表,逐个元素遍历即可
    node = listFirst(keys);
    while (node) {
        robj *kobj = listNodeValue(node);
        nextnode = listNextNode(node);
        int filter = 0;

        if (!filter && use_pattern) {

            // 是sds对象的话,直接比较字符串
            if (sdsEncodedObject(kobj)) {
                if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
                    filter = 1;
            } else { // 否则把数字对象,转换为string后再比较字符串
                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;
            }
        }

        /* Filter an element if it isn't the type we want. */
        if (!filter && o == NULL && typename){
            robj* typecheck = lookupKeyReadWithFlags(c->db, kobj, LOOKUP_NOTOUCH);
            char* type = getObjectTypeName(typecheck);
            if (strcasecmp((char*) typename, type)) filter = 1;
        }

        // 判断是否键过期,过期的话,filter置为1
        if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;

        // 过期的话则删除
        if (filter) {
            decrRefCount(kobj);         // 减少引用次数
            listDelNode(keys, node);    // 从list中删除这个节点
        }

        // 是zset,hash对象时,如果需要被删除,这里还需要删除value
        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;
    }

    // 回复信息,注意这里把cursor返回给了客户端
    addReplyArrayLen(c, 2);
    addReplyBulkLongLong(c,cursor);

    addReplyArrayLen(c, listLength(keys));
    
    // 整理keys,一一压入回复消息
    while ((node = listFirst(keys)) != NULL) {
        robj *kobj = listNodeValue(node);
        addReplyBulk(c, kobj);
        decrRefCount(kobj);
        listDelNode(keys, node);
    }

// 清理操作
cleanup:
    listSetFreeMethod(keys,decrRefCountVoid);
    listRelease(keys);
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值