简言
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);
}