这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战
起因
昨天在群里看到了一道面试题,题目是:
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的
如果将它们全部找出来并且说明使用的方法的底层原理?
复制代码
看群里的小伙伴们各抒己见,最后总结起来有如下三个命令:
- keys:用于查找所有符合给定模式
pattern
的key
- smembers:返回集合
key
中的所有成员 - scan:增量迭代当前数据库中的所有数据库键
其中,keys
和smembers
命令都是返回所有成员,时间复杂度为
。scan
虽然时间复杂度也是
,但是它采用的是增量的方式,数据量较大的场景下,不会像前两者一样一直阻塞着线程。
上面都是通过网上搜索来的答案,具体是不是这样我也不确定。于是我跑去github
上面下了redis
的源码,看看源码里面是怎么样的。
源码
在源码文件中有一个命令表,位于redis\src\server.c
文件中,在这里我们可以快速的找到各个命令对应的函数入口。
命令的入口找到了,我们就可以分别跳转过去看它的对应实现了。
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++的语法不熟悉,备注都是阅读源码后结合注释进行理解的,如果文中有备注错误的地方,希望大佬们可以帮忙指点一下!