redis的scan命令的源码分析,实现原理
生活随笔
收集整理的這篇文章主要介紹了
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 有值,說明對象一定是hash, set 或者 zset;如果參數 o 為空,說明是遍歷當前整個數據庫 void scanGenericCommand(client *c, robj *o, unsigned long cursor) {int i, j;// 新建一個list,用來暫存一定量的未篩選的keylist *keys = listCreate();listNode *node, *nextnode;// 如果客戶端不傳count值,那么默認是10long 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;}// 因為參數是成對的,所以跳2i += 2;} // 解析參數 match,不區分大小寫else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {pat = c->argv[i+1]->ptr;patlen = sdslen(pat);// 若是*,那么use_pattern置為1use_pattern = !(pat[0] == '*' && patlen == 1);// 因為參數是成對的,所以跳2i += 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];// 最多遍歷次數,防止過多占用cpulong 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 置為0cursor = 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 置為0cursor = 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置為1if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;// 過期的話則刪除if (filter) {decrRefCount(kobj); // 減少引用次數listDelNode(keys, node); // 從list中刪除這個節點}// 是zset,hash對象時,如果需要被刪除,這里還需要刪除valueif (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); }總結
以上是生活随笔為你收集整理的redis的scan命令的源码分析,实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: json中omitempty字段的使用
- 下一篇: netstat命令总结