Redis中keys, scan, smembers命令的区别

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

起因

昨天在群里看到了一道面试题,题目是:

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的
如果将它们全部找出来并且说明使用的方法的底层原理?
复制代码

看群里的小伙伴们各抒己见,最后总结起来有如下三个命令:

  • keys:用于查找所有符合给定模式 patternkey
  • smembers:返回集合 key 中的所有成员
  • scan:增量迭代当前数据库中的所有数据库键

其中,keyssmembers命令都是返回所有成员,时间复杂度为 O ( n ) O(n) scan虽然时间复杂度也是 O ( n ) O(n) ,但是它采用的是增量的方式,数据量较大的场景下,不会像前两者一样一直阻塞着线程。

上面都是通过网上搜索来的答案,具体是不是这样我也不确定。于是我跑去github上面下了redis的源码,看看源码里面是怎么样的。

github.com/redis/redis

源码

在源码文件中有一个命令表,位于redis\src\server.c文件中,在这里我们可以快速的找到各个命令对应的函数入口。

image.png

image.png

命令的入口找到了,我们就可以分别跳转过去看它的对应实现了。

keys

keys命令函数中可以看到它是遍历了整个键空间字典,将与给定的pattern匹配的添加到返回列表中。

void keysCommand(client *c) {
    dictIterator *di;
    dictEntry *de;
    sds pattern = c->argv[1]->ptr;
    int plen = sdslen(pattern), allkeys;
    unsigned long numkeys = 0;
    void *replylen = addReplyDeferredLen(c);

    di = dictGetSafeIterator(c->db->dict);
    allkeys = (pattern[0] == '*' && plen == 1);
    while((de = dictNext(di)) != NULL) {
        sds key = dictGetKey(de);
        robj *keyobj;

        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            if (!keyIsExpired(c->db,keyobj)) {
                addReplyBulk(c,keyobj);
                numkeys++;
            }
            decrRefCount(keyobj);
        }
    }
    dictReleaseIterator(di);
    setDeferredArrayLen(c,replylen,numkeys);
}
复制代码

smembers

smembers命令这边调用的是sinterGenericCommand函数,搜索范围传递的是0,表示全局搜索。

/* SINTER key [key ...] */
void sinterCommand(client *c) {
    sinterGenericCommand(c, c->argv+1,  c->argc-1, NULL, 0, 0);
}

/* SINTER / SINTERSTORE / SINTERCARD
 *
 * 'cardinality_only' work for SINTERCARD, 只返回具有最小处理和内存开销的基数.
 *
 * 'limit' work for SINTERCARD, 达到限制后停止搜索。传递 0 表示无限制.
 */
void sinterGenericCommand(client *c, robj **setkeys,
                          unsigned long setnum, robj *dstkey,
                          int cardinality_only, unsigned long limit) {}
复制代码

scan

由于scanGenericCommand代码太长来这里就不全贴出来,小伙伴们感兴趣的话可以下载源码搜索一下。

该函数的大概实现逻辑应该是先解析附带的参数,得到需要返回的数据长度以及游标值,再迭代集合获取所需数据。

具体的入口函数为disScan

/* 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);
}

/* This command implements SCAN, HSCAN and SSCAN commands.
 * 如果对象 'o' 被传递,那么它必须是一个 Hash、Set 或 Zset 对象,
 * 否则如果 'o' 为 NULL,该命令将对与当前数据库关联的字典进行操作
 *
 * 当 'o' 不为 NULL 时,该函数假定客户端参数向量中的第一个参数是一个键,
 * 因此它会在迭代之前跳过它以解析选项。
 *
 * 在 Hash 对象的情况下,该函数返回 Hash 上每个元素的字段和值。 */
void scanGenericCommand(client *c, robj *o, unsigned long cursor) {}
复制代码

最后

由于我是Java出身,对于c/c++的语法不熟悉,备注都是阅读源码后结合注释进行理解的,如果文中有备注错误的地方,希望大佬们可以帮忙指点一下!

猜你喜欢

转载自juejin.im/post/7032231036142813214