一文讀懂Redis6的--bigkeys選項(xiàng)源碼以及redis-bigkey-online項(xiàng)目介紹

一文讀懂Redis6的--bigkeys選項(xiàng)源碼以及redis-bigkey-online項(xiàng)目介紹

本文分為兩個(gè)部分,第一是詳細(xì)講解Redis6的--bigkeys選項(xiàng)相關(guān)源碼是怎樣實(shí)現(xiàn)的嵌屎,第二部分為自己對--bigkeys源碼的優(yōu)化項(xiàng)目redis-bigkey-online的介紹。redis-bigkey-online是自己開發(fā)的非常好用、高效的bigkey查找工具,因?yàn)槭切薷牡脑创a,所以是直接整合在redis-cli程序中据某,由官方的

 ./redis-cli --bigkeys

改為

./redis-cli --bigkeys redis-bigkey-online.conf

即可使用,redis-bigkey-online.conf則保存了用戶的個(gè)性化設(shè)定诗箍,包括需要輸出哪些類型的bigkey癣籽、輸出前N個(gè)bigkey、設(shè)定bigkey判斷閾值等功能扳还。并且才避,由于自己修改源碼一直遵循“盡量少改、盡量集中改氨距、盡量改的部分風(fēng)格和源碼統(tǒng)一”三個(gè)“盡量”的原則桑逝,所以該項(xiàng)目也十分容易的移植到其他版本的redis上。歡迎大家star和使用~

--bigkeys選項(xiàng)源碼原理解析

首先我們從運(yùn)行結(jié)果出發(fā)俏让。首先通過腳本插入一些數(shù)據(jù)到redis中楞遏,然后執(zhí)行redis-cli的--bigkeys選項(xiàng)

[root@ecs-7e58 add-nomal-key]# redis-cli --bigkeys -h 127.0.0.1 -p 6379

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest zset   found so far '"zset_32_4769"' with 10 members
[00.00%] Biggest set    found so far '"set_32_1808"' with 10 members
[00.00%] Biggest list   found so far '"list_32_3402"' with 10 items
[00.00%] Biggest string found so far '"string_32_1957"' with 32 bytes
[00.00%] Biggest hash   found so far '"hash_32_1481"' with 10 fields

-------- summary -------

Sampled 50000 keys in the keyspace!
Total key length in bytes is 604470 (avg len 12.09)

Biggest   list found '"list_32_3402"' has 10 items
Biggest   hash found '"hash_32_1481"' has 10 fields
Biggest string found '"string_32_1957"' has 32 bytes
Biggest    set found '"set_32_1808"' has 10 members
Biggest   zset found '"zset_32_4769"' has 10 members

10000 lists with 100000 items (20.00% of keys, avg size 10.00)
10000 hashs with 100000 fields (20.00% of keys, avg size 10.00)
10000 strings with 320000 bytes (20.00% of keys, avg size 32.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
10000 sets with 100000 members (20.00% of keys, avg size 10.00)
10000 zsets with 100000 members (20.00% of keys, avg size 10.00)

注意summary下面的信息,分別是總的key的統(tǒng)計(jì)信息首昔,然后是每種數(shù)據(jù)類型中top1的那個(gè)key寡喝,最后是各種數(shù)據(jù)結(jié)構(gòu)的統(tǒng)計(jì)數(shù)據(jù)±掌妫可以看到预鬓,雖然--bigkeys選項(xiàng)會(huì)掃描整個(gè)redis,但是只輸出每種數(shù)據(jù)類型top1的那個(gè)key赊颠。但是實(shí)際卻和我們找bigkey的需求相去甚遠(yuǎn)格二,實(shí)際我們可能需要前N個(gè)bigkey劈彪,并且bigkey的閾值也是可以自己設(shè)定的。所以我們有了改源碼的需求顶猜,自然在改源碼之前需要對源碼的實(shí)現(xiàn)原理有所掌握才行沧奴。

由運(yùn)行結(jié)果我們會(huì)猜想redis可能是維護(hù)了6個(gè)變量用來記錄每種數(shù)據(jù)類型的topkey,如果遍歷時(shí)遇到更大的就替換之前的长窄,這和在數(shù)組中找到最大值的原理是一樣的滔吠,而實(shí)際上redis確實(shí)也是這樣做的。

redis找bigkey的函數(shù)是static void findBigKeys(int memkeys, unsigned memkeys_samples)挠日,因?yàn)?-memkeys選項(xiàng)和--bigkeys選項(xiàng)是公用同一個(gè)函數(shù)疮绷,所以使用memkeys時(shí)會(huì)有額外兩個(gè)參數(shù)memkeys、memkeys_sample肆资,但這和--bigkeys選項(xiàng)沒關(guān)系矗愧,所以不用理會(huì)。findBigKeys具體函數(shù)框架為:

findBigKeys:
1.申請6個(gè)變量用以統(tǒng)計(jì)6種數(shù)據(jù)類型的信息(每個(gè)變量記錄該數(shù)據(jù)類型的key的總數(shù)量郑原、bigkey是哪個(gè)等信息)
2.調(diào)用scan命令迭代地獲取一批key(注意只是key的名稱,類型和大小scan命令不返回)
3.對每個(gè)key獲取它的數(shù)據(jù)類型(type)和key的大幸固椤(size)
4.對每個(gè)key更新對應(yīng)數(shù)據(jù)類型的統(tǒng)計(jì)信息
5.如果key的大小大于已記錄的最大值的key犯犁,則更新最大key的信息
6.回到步驟2,直到遍歷完所有key
7.輸出統(tǒng)計(jì)信息女器、最大key信息

1.申請6個(gè)變量用以統(tǒng)計(jì)各類型的統(tǒng)計(jì)信息

首先是第一步酸役,申請6個(gè)變量:

dict *types_dict = dictCreate(&typeinfoDictType, NULL);
typeinfo_add(types_dict, "string", &type_string);
typeinfo_add(types_dict, "list", &type_list);
typeinfo_add(types_dict, "set", &type_set);
typeinfo_add(types_dict, "hash", &type_hash);
typeinfo_add(types_dict, "zset", &type_zset);
typeinfo_add(types_dict, "stream", &type_stream);

dictCreate函數(shù)創(chuàng)建了一個(gè)字典變量types_dict,然后通過typeinfo_add向這個(gè)字典中添加6個(gè)dictEntry結(jié)構(gòu)驾胆。這里的dictEntry其實(shí)就是一個(gè)kv對結(jié)構(gòu)涣澡,k保存數(shù)據(jù)類型名稱,如記錄string信息的dictEntry的key就是"string"丧诺,而v才是真正用來保存統(tǒng)計(jì)信息的地方入桂。不知道什么是dict的同學(xué)可以看下下面字典結(jié)構(gòu)的示意圖,dict是redis最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)之一驳阎。

image-20210127104135798.png

其實(shí)dictEntry的v字段是一個(gè)union變量抗愁,如下所示:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

如果v是整數(shù)就保存在v.u64或者v.s64,浮點(diǎn)數(shù)就保存在v.d呵晚,而如果v是復(fù)雜點(diǎn)的數(shù)據(jù)比如這里的6個(gè)dictEntry的v字段既要保存該數(shù)據(jù)類型的一些統(tǒng)計(jì)信息又要記錄該數(shù)據(jù)類型的最大的key是誰蜘腌,那么只有新建一種結(jié)構(gòu)體typeinfo,并通過dictEntry的v.val字段指向typeinfo結(jié)構(gòu)體饵隙。

字典types_dict里面保存了6個(gè)kv對(dictEntry)撮珠,每個(gè)dictEntry的v的初始值為type_xxx常量,下面是typeinfo的結(jié)構(gòu)定義以及各type_xxx的值:

typedef struct {
    char *name;//數(shù)據(jù)類型金矛,如string
    char *sizecmd;//查詢大小命令芯急,如string會(huì)調(diào)用STRLEN
    char *sizeunit;//單位勺届,string類型為bytes,而hash為field
    unsigned long long biggest;//最大key信息域志于,此數(shù)據(jù)類型最大key的大小涮因,如string類型是多少bytes,hash為多少field
    unsigned long long count;//統(tǒng)計(jì)信息域伺绽,此數(shù)據(jù)類型的key的總數(shù)
    unsigned long long totalsize;//統(tǒng)計(jì)信息域养泡,此數(shù)據(jù)類型的key的總大小,如string類型是全部string總共多少bytes奈应,hash為全部hash總共多少field
    sds biggest_key;//最大key信息域澜掩,此數(shù)據(jù)類型最大key的鍵名,之所以在數(shù)據(jù)結(jié)構(gòu)末尾是考慮字節(jié)對齊
} typeinfo;

typeinfo type_string = { "string", "STRLEN", "bytes" };
typeinfo type_list = { "list", "LLEN", "items" };
typeinfo type_set = { "set", "SCARD", "members" };
typeinfo type_hash = { "hash", "HLEN", "fields" };
typeinfo type_zset = { "zset", "ZCARD", "members" };
typeinfo type_stream = { "stream", "XLEN", "entries" };
typeinfo type_other = { "other", NULL, "?" };

name字段是用來記錄該結(jié)構(gòu)體記錄的那種數(shù)據(jù)類型杖挣,sizecmd用來記錄對此種數(shù)據(jù)類型改用什么命令來查詢其大小肩榕,sizeunit則是該數(shù)據(jù)類型的大小單位,而count惩妇、totalsize則是記錄一些統(tǒng)計(jì)信息株汉,遍歷到某個(gè)key的時(shí)候,無論是不是bigkey歌殃,都會(huì)更新counttotalsize乔妈,biggest_key記錄最大key是誰,biggest則記錄這個(gè)最大key有多大氓皱。之所以type_string等常量只有前三個(gè)域的值路召,是因?yàn)閎iggest、count等域只有在遍歷時(shí)才會(huì)產(chǎn)生并發(fā)生改變波材,初始是不知道的股淡。

其實(shí)按效率上來講可以完全不用dict結(jié)構(gòu),直接用一個(gè)大小為6的typeinfo數(shù)組就行廷区,但是作者或許對自己的字典結(jié)構(gòu)很自豪所以就不用其他數(shù)據(jù)結(jié)構(gòu)了唯灵。事實(shí)當(dāng)你了解字典結(jié)構(gòu)的細(xì)節(jié)后也會(huì)愛上它(●'?'●)

緊接著是獲取數(shù)據(jù)庫總大小和輸出一些前置消息:

/* Total keys pre scanning */
total_keys = getDbSize();

/* Status message */
printf("\n# Scanning the entire keyspace to find biggest keys as well as\n");
printf("# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec\n");
printf("# per 100 SCAN commands (not usually needed).\n\n");

total_keys保存數(shù)據(jù)庫總key數(shù)

2.調(diào)用scan命令迭代地獲取一批key

之所以用scan命令而不用keys命令是因?yàn)閗eys命令雖然可以一次性返回所有key躲因,但是由于redis執(zhí)行命令的時(shí)候是單線程模型早敬,數(shù)據(jù)庫過大的話會(huì)嚴(yán)重阻塞服務(wù)器,因而使用scan命令一次獲取部分key然后再迭代獲取下一批key這樣更好大脉。

/* SCAN loop */
do {
    /* Calculate approximate percentage completion */
    pct = 100 * (double)sampled/total_keys;//這里記錄下掃描的進(jìn)度

    /* Grab some keys and point to the keys array */
    reply = sendScan(&it);//這里發(fā)送SCAN命令搞监,結(jié)果保存在reply中
    keys  = reply->element[1];//keys來保存這次scan獲取的所有鍵名,注意只是鍵名镰矿,每個(gè)鍵的數(shù)據(jù)類型是不知道的琐驴。

    ......

} while(it != 0);          

sampled記錄已經(jīng)遍歷的key數(shù)量,pct則為百分比進(jìn)度。reply保存scan命令的結(jié)果绝淡。為什么是reply->element[1]保存了所有鍵名呢宙刘?怕小伙伴忘記了scan命令,這里再解釋下牢酵,scan命令返回值如下(后續(xù)很多地方會(huì)用到這里的運(yùn)行結(jié)果):

127.0.0.1:6379> scan 0
1) "20480"
2)  1) "zset_32_4769"
    2) "set_32_1808"
    3) "zset_32_9252"
    4) "list_32_3402"
    5) "set_32_5036"
    6) "string_32_1957"
    7) "string_32_2471"
    8) "hash_32_1481"
    9) "hash_32_853"
   10) "string_32_2945"

scan 0表示從數(shù)據(jù)庫開頭獲取一批key悬包,返回的第一個(gè)值是下一次迭代的值,下一次scan命令就是scan 20480馍乙,這樣就可以保證獲取的下一批key和這一批是不一樣的布近,sendScan(&it)it既是輸入值也是輸出值,比如上面輸入的時(shí)候是0丝格,執(zhí)行完后是20480撑瞧。同時(shí)reply->element[0]也為下次迭代的值,reply->element[1]則保存scan獲取的所有鍵名显蝌。

這里在解釋下reply的數(shù)據(jù)結(jié)構(gòu)预伺,以方便后續(xù)代碼理解。reply的數(shù)據(jù)結(jié)構(gòu)是redisReply

/* This is the reply object returned by redisCommand() */
typedef struct redisReply {
    int type; /* REDIS_REPLY_* */
    long long integer; /* 當(dāng)type為REDIS_REPLY_INTEGER曼尊,這里保存整數(shù) */
    double dval; /* 當(dāng)type為REDIS_REPLY_DOUBLE酬诀,這里保存浮點(diǎn)數(shù) */
    size_t len; /* string的長度 */
    char *str; /* Used for REDIS_REPLY_ERROR, REDIS_REPLY_STRING
                  and REDIS_REPLY_DOUBLE (in additionl to dval). */
    char vtype[4]; /* Used for REDIS_REPLY_VERB, contains the null
                      terminated 3 character content type, such as "txt". */
    size_t elements; /* elements的數(shù)量, for REDIS_REPLY_ARRAY */
    struct redisReply **element; /* 當(dāng)type為REDIS_REPLY_ARRAY,保存返回的向量 */
} redisReply;

type表示命令返回值的類型骆撇,如果命令返回的是整數(shù)料滥,比如strlen命令返回值是整數(shù),那么type的值就為REDIS_REPLY_INTEGER艾船,而interger域則保存了這個(gè)整數(shù)。同理當(dāng)type為REDIS_REPLY_ARRAY時(shí)高每,elements域保存該數(shù)組的長度屿岂,比如上面scan命令返回的reply->elements就是2,最后一個(gè)域struct redisReply **element可能有點(diǎn)難理解鲸匿,其實(shí)就是一個(gè)指針數(shù)組爷怀,數(shù)組的每個(gè)元素是一個(gè)redisReply*指針,這里還是通過上面scan命令畫出內(nèi)存結(jié)構(gòu)圖:

image-20210127130938852.png

這里可以很清楚地看到带欢,reply->element[0]指向一個(gè)redisReply結(jié)構(gòu)體运授,用以保存下一次scan的迭代值,而reply->element[1]也指向一個(gè)redisReply結(jié)構(gòu)體乔煞,此結(jié)構(gòu)體保存了本次scan獲取的所有key的鍵名吁朦。

3.對每個(gè)key獲取它的數(shù)據(jù)類型(type)和key的大小(size)

通過scan命令得到reply渡贾、keys = reply->element[1]得到這批鍵名后逗宜,就可以通過鍵名去獲取它的類型(type)和大小(size):

/* Retrieve types and then sizes */
getKeyTypes(types_dict, keys, types);
getKeySizes(keys, types, sizes, memkeys, memkeys_samples);

types是一個(gè)typeinfo*的指針數(shù)組,sizes則為unsigned long long的數(shù)組纺讲。每個(gè)scan循環(huán)開始它們都是空的擂仍,如下圖所示:

image-20210201170731218.png

getKeyTypes(types_dict, keys, types)函數(shù)則是對keys中的每個(gè)key,通過TYPE {keyname}的形式獲取該key的類型并使types中的元素指向?qū)?yīng)的type_info結(jié)構(gòu)體:

image-20210201171251587.png

之后通過types就可以獲得對應(yīng)的sizecmd熬甚,于是getKeySizes(keys, types, sizes, memkeys, memkeys_samples)就是通過{sizecmd} {keyname}的形式獲取每個(gè)key的大小逢渔,比如圖中zset_32_4769這個(gè)key我們可以通過ZCARD zset_32_4769獲取到它的size為10。結(jié)果如下:

image-20210201171826116.png

memkeys乡括、 memkeys_samples參數(shù)肃廓,和--bigkeys無關(guān),和--memkeys選項(xiàng)有關(guān)粟判,這里不再贅述亿昏。

4.對每個(gè)key更新對應(yīng)數(shù)據(jù)類型的統(tǒng)計(jì)信息

有了types和sizes后,就可以來更新各typeinfo結(jié)構(gòu)體變量了档礁。

/* Now update our stats */
for(i=0;i<keys->elements;i++) {
    typeinfo *type = types[i];
    /* Skip keys that disappeared between SCAN and TYPE */
    if(!type)
        continue;

    //對每個(gè)key更新每種數(shù)據(jù)類型的統(tǒng)計(jì)信息
    type->totalsize += sizes[i];//某數(shù)據(jù)類型(如string)的總大小增加
    type->count++;//某數(shù)據(jù)類型的key數(shù)量增加
    totlen += keys->element[i]->len;//totlen不針對某個(gè)具體數(shù)據(jù)類型角钩,將所有key的鍵名的長度進(jìn)行統(tǒng)計(jì),注意只統(tǒng)計(jì)鍵名長度呻澜。
    sampled++;//已經(jīng)遍歷的key數(shù)量

    ......//后續(xù)解析

    /* Update overall progress */
    if(sampled % 1000000 == 0) {
        printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
    }
}

不管該key是不是bigkey递礼,totalsize記錄該類型的所有key的總大小,count則記錄有多少key羹幸。而totlen變量不屬于typrinfo結(jié)構(gòu)體脊髓,它只是用來記錄所有類型的所有key的鍵名的總長度,加入一個(gè)數(shù)據(jù)庫只有兩個(gè)key:string_1栅受、hash_3将硝,那么totlen就是8+6=14。sampled之前說過屏镊,就是來記錄已經(jīng)遍歷到第幾個(gè)key了依疼,用來計(jì)算進(jìn)度信息。

5.如果key的大小大于已記錄的最大值的key而芥,則更新最大key的信息

/* Now update our stats */
for(i=0;i<keys->elements;i++) {
    ......//前面已解析

    //如果遍歷到比記錄值更大的key時(shí)
    if(type->biggest<sizes[i]) {
        /* Keep track of biggest key name for this type */
        if (type->biggest_key)
            sdsfree(type->biggest_key);
        //更新最大key的鍵名
        type->biggest_key = sdscatrepr(sdsempty(), keys->element[i]->str, keys->element[i]->len);
        if(!type->biggest_key) {
            fprintf(stderr, "Failed to allocate memory for key!\n");
            exit(1);
        }

        //每當(dāng)找到一個(gè)更大的key時(shí)則輸出該key信息
        printf(
            "[%05.2f%%] Biggest %-6s found so far '%s' with %llu %s\n",
            pct, type->name, type->biggest_key, sizes[i],
            !memkeys? type->sizeunit: "bytes");

        /* Keep track of the biggest size for this type */
        //更新最大key的大小
        type->biggest = sizes[i];
    }

    ......//前面已解析
}

if(type->biggest<sizes[i])表示該typeinfo結(jié)構(gòu)體已記錄的最大key的大小如果小于正在遍歷到的key的大小時(shí)律罢,則進(jìn)行更新替換。因?yàn)閠ype->biggest_key是字符串指針棍丐,所以需要先free掉舊的字符串然后新建一個(gè)字符串并讓type->biggest_key指向它误辑。更新了type->biggest_key后便同時(shí)更新下type->biggest

到這里一個(gè)scan循環(huán)還沒結(jié)束歌逢,scan循環(huán)最后會(huì)執(zhí)行以下代碼:

/* Sleep if we've been directed to do so */
if(sampled && (sampled %100) == 0 && config.interval) {
    usleep(config.interval);
}

如果設(shè)置了每次scan命令的間隔巾钉,則一次scan完后會(huì)睡眠一段時(shí)間再執(zhí)行scan循環(huán),呼應(yīng)最開始的/* Status message */趋翻。

7.輸出統(tǒng)計(jì)信息睛琳、最大key信息

2~5步為一個(gè)scan循環(huán)盒蟆,直到最后一次scan返回的迭代值為0時(shí)結(jié)束。接著就可以進(jìn)行結(jié)果是輸出了:

/* We're done */
printf("\n-------- summary -------\n\n");

printf("Sampled %llu keys in the keyspace!\n", sampled);
printf("Total key length in bytes is %llu (avg len %.2f)\n\n",
       totlen, totlen ? (double)totlen/sampled : 0);

首先輸出總共掃描了多少個(gè)key师骗、所有key的總長度是多少历等。

/* Output the biggest keys we found, for types we did find */
di = dictGetIterator(types_dict);
while ((de = dictNext(di))) {
    typeinfo *type = dictGetVal(de);
    
    if(type->biggest_key) {
        printf("Biggest %6s found '%s' has %llu %s\n", type->name, type->biggest_key,
               type->biggest, !memkeys? type->sizeunit: "bytes");
    }
}
dictReleaseIterator(di);

di為字典迭代器,用以遍歷types_dict里面的所有dictEntry辟癌。de = dictNext(di)則可以獲取下一個(gè)dictEntry寒屯,de是指向dictEntry的指針。又因?yàn)閠ypeinfo結(jié)構(gòu)體保存在dictEntry的v域中黍少,所以用dictGetVal獲取寡夹。然后就是輸出typeinfo結(jié)構(gòu)體里面保存的最大key相關(guān)的數(shù)據(jù),包括最大key的鍵名和大小厂置。

di = dictGetIterator(types_dict);
while ((de = dictNext(di))) {
    typeinfo *type = dictGetVal(de);
    
    printf("%llu %ss with %llu %s (%05.2f%% of keys, avg size %.2f)\n",
           type->count, type->name, type->totalsize, !memkeys? type->sizeunit: "bytes",
           sampled ? 100 * (double)type->count/sampled : 0,
           type->count ? (double)type->totalsize/type->count : 0);
}
dictReleaseIterator(di);

這里的dict操作和上一步類似菩掏,不在贅述。只是這個(gè)循環(huán)輸出的是typeinfo結(jié)構(gòu)體里面的統(tǒng)計(jì)信息而非最大key信息昵济。

dictRelease(types_dict);

findBigKeys的最后再釋放掉開頭申請的字典智绸,以結(jié)束整個(gè)找bigkey的流程。

redis-bigkey-online

終于將--bigkeys選項(xiàng)的源碼講完了~那么現(xiàn)在就開始正式介紹redis-bigkey-online項(xiàng)目访忿,項(xiàng)目地址會(huì)放在文末瞧栗。下面將從設(shè)計(jì)思路、具體代碼海铆、使用方法迹恐、性能比較四個(gè)方面進(jìn)行講解。

設(shè)計(jì)思路

設(shè)計(jì)思路其實(shí)很簡單卧斟∨贡撸看完了前面--bigkeys源碼我們可以發(fā)現(xiàn),redis作者本身其實(shí)就是用了5個(gè)typeinfo保存各數(shù)據(jù)類型的信息珍语,但是遺憾的是作者只保存了每種數(shù)據(jù)類型top1的一個(gè)key找都,每次掃描到較大的key時(shí)會(huì)對舊的bigkey進(jìn)行替換。所以我就想能不能保存前N個(gè)大key而不只是top1廊酣,自然第一時(shí)間想到了大/小頂堆。根據(jù)用戶的設(shè)定維護(hù)一個(gè)長度N的大/小頂堆赏枚,當(dāng)數(shù)據(jù)數(shù)量小于N時(shí)直接插入就好了亡驰,當(dāng)數(shù)據(jù)滿時(shí)將正在掃描的key和堆中最小值進(jìn)行比較,如果小于堆中最小值就直接跳過饿幅,如果大于就先刪除堆中最小值然后再將掃描的key插入凡辱。并且堆也十分適合用線性空間來實(shí)現(xiàn),十分節(jié)省空間栗恩。

然而堆插入數(shù)據(jù)時(shí)透乾,雖然空間復(fù)雜度小,但是插入元素時(shí)調(diào)整堆的時(shí)間復(fù)雜度時(shí)O(nlgn)。我在想有沒有更快的帶排序功能的數(shù)據(jù)結(jié)構(gòu)乳乌,這時(shí)候就突然想到了redis自己的數(shù)據(jù)類型——zset捧韵!zset和set的區(qū)別在于set里的元素只是元素自身,而zset的每個(gè)元素還帶有分?jǐn)?shù)(score)汉操,zset會(huì)根據(jù)元素的score對元素進(jìn)行自動(dòng)排列再来,十分適合我的需求,score保存bigkey的大小磷瘤、member保存該bigkey的鍵名芒篷!而zset的底層數(shù)據(jù)結(jié)構(gòu)之一就是喜聞樂見的跳躍表!其插入元素的時(shí)間復(fù)雜度度為O(lgn)采缚!雖然空間復(fù)雜度相較堆多了不少针炉,但是我們找bigkey也就是想找其中的幾個(gè)數(shù)據(jù),不可能數(shù)據(jù)庫全部數(shù)據(jù)都是bigkey扳抽!

關(guān)于跳表的介紹參照這篇博文:一文徹底搞懂跳表的各種時(shí)間復(fù)雜度篡帕、適用場景以及實(shí)現(xiàn)原理

skiplist作為zset的存儲(chǔ)結(jié)構(gòu),整體存儲(chǔ)結(jié)構(gòu)如下圖摔蓝。核心點(diǎn)主要包括一個(gè)dict對象和一個(gè)skiplist對象赂苗。dict保存key/value,key為元素贮尉,value為分值拌滋;skiplist保存的有序的元素列表,每個(gè)元素包括元素和分值猜谚。skiplist和dict并不是獨(dú)立的數(shù)據(jù)結(jié)構(gòu)败砂,skiplistNode的ele和dictEntry的key是指向了同一sds字符串,就是說skiplist主要負(fù)責(zé)各元素間的大小排列關(guān)系魏铅;而dict則負(fù)責(zé)鍵名和分?jǐn)?shù)之間的映射關(guān)系昌犹,從而可以在O(1)的時(shí)間復(fù)雜度找到對應(yīng)的數(shù)據(jù)。關(guān)鍵是览芳,我還不用重新寫zset數(shù)據(jù)類型的代碼斜姥,直接使用源碼的zset相關(guān)數(shù)據(jù)結(jié)構(gòu)就行了!(?′?`?)

20200918235136825.png

具體代碼

理想很豐滿沧竟,現(xiàn)實(shí)卻很殘酷铸敏,zset相關(guān)源碼確實(shí)可以用,但是不能直接用悟泵。redis里面有很多很優(yōu)秀的數(shù)據(jù)結(jié)構(gòu)杈笔,比如sds動(dòng)態(tài)字符串、dict字典糕非、ziplist壓縮列表等等以及skiplist跳躍表蒙具。有些數(shù)據(jù)結(jié)構(gòu)適用性很強(qiáng)比如sds球榆、dict,不僅redis-server程序會(huì)用到禁筏,redis-cli程序也會(huì)用到持钉,所以sds、dict相關(guān)代碼單獨(dú)形成一個(gè)文件sds.c融师、dict.c并且函數(shù)聲明在sds.hdict.h右钾,server.c、redis-cli.c中只要#include "sds.h"旱爆、#include "dict.h"就可以使用該數(shù)據(jù)結(jié)構(gòu)舀射。然而有些數(shù)據(jù)結(jié)構(gòu)就比如這里的skiplist,作者認(rèn)為只有服務(wù)端程序redis-server會(huì)用到怀伦,客戶端程序redis-cli不會(huì)用到脆烟,所以根本就沒有skiplist.hskiplist.c,skiplist的聲明是直接寫在server.h中房待,skiplist的函數(shù)實(shí)現(xiàn)則寫在t_zset.c中邢羔。你或許會(huì)說,那redis-cli.c中你直接#include "server.h"并且makefile里面鏈接形成redis-cli程序時(shí)八t_zset.o鏈接進(jìn)來不行嗎桑孩?

不行拜鹤!達(dá)妹喲!

server.h里面有很多是服務(wù)器端程序會(huì)用到的函數(shù)聲明比如usage()流椒、mstime()敏簿、utime()等會(huì)和redis-cli.c里的同名函數(shù)發(fā)生函數(shù)沖突,并且t_zset.c中也使用了大量的server.c中的函數(shù)宣虾,如果鏈接程序時(shí)只鏈接t_zset.o會(huì)報(bào)錯(cuò)提示大量的函數(shù)未定義的錯(cuò)誤惯裕!這時(shí)候再心存僥幸說鏈接形成redis-cli程序時(shí)把server.o也鏈接進(jìn)來行不行?這樣就更離譜了绣硝!server.c是服務(wù)端程序的主文件蜻势,里面有main函數(shù)入口!redis-cli.c是客戶端程序的主文件鹉胖,里面有main函數(shù)入口握玛!這種低級函數(shù)沖突是不該犯的!

所以主要問題是zset和server.c的耦合性太高了甫菠!現(xiàn)在只能去閱讀zset败许、skiplist相關(guān)源碼,將重要的代碼提煉出來淑蔚,形成一個(gè)和server.c、redis-cli.c相互獨(dú)立的一個(gè)文件愕撰,這樣redis-cli.c就可以開開心心地去使用啦~也希望redis作者能將眾數(shù)據(jù)結(jié)構(gòu)代碼進(jìn)行解耦操作刹衫,不要只有sds和dict是獨(dú)立的醋寝。

提取代碼其實(shí)不麻煩,并不是所有有關(guān)代碼都需要带迟,并且絕大部分代碼直接cv下來就行音羞,我們只需要認(rèn)真閱讀源碼,將zset的一些關(guān)鍵函數(shù)提煉出來就行仓犬。我將提煉的代碼寫在了zset.hzset.c中:

//zset.h
//數(shù)據(jù)結(jié)構(gòu)
#include "dict.h"
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

//函數(shù)聲明
zskiplistNode *zslCreateNode(int level, double score, sds ele);
zskiplist *zslCreate(void);
void zslFreeNode(zskiplistNode *node);
void zslFree(zskiplist *zsl);
int zslRandomLevel(void);
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele);
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore);
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update);
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node);

zset *zsetCreate(void);
void zsetFree(zset *zs);
unsigned long zsetLength(const zset *zs);
sds zsetMin(const zset *zs);
sds zsetMax(const zset *zs);
int zsetScore(zset *zs, sds member, double *score);
int zsetAdd(zset *zs, double score, sds ele);
int zsetDel(zset *zs, sds ele);

可以看到基本都是些增嗅绰、刪、改搀继、查相關(guān)的函數(shù)窘面,因?yàn)閦set底層是skiplist和dict,dict因?yàn)樽髡咭呀?jīng)做了解耦操作叽躯,所以直接#include "dict.h"就行财边,這里只是將跳表相關(guān)的數(shù)據(jù)結(jié)構(gòu)提取了出來。在這里自己只還新增了zsetMin点骑、zsetMax兩個(gè)函數(shù)酣难。zset底層編碼有兩種:skiplist和ziplist,這里將zset函數(shù)中所有ziplist相關(guān)的函數(shù)都進(jìn)行了剔除工作黑滴,只保留了skiplist部分憨募。仔細(xì)的同學(xué)會(huì)發(fā)現(xiàn)為什么skiplist相關(guān)函數(shù)沒有zskiplistFind呢?這個(gè)問題很好回答袁辈,仔細(xì)看zset結(jié)構(gòu)的編碼菜谣,它包含一個(gè)zskiplist和dict,zskiplist只負(fù)責(zé)元素間的排序關(guān)系吵瞻,而元素和分?jǐn)?shù)的映射關(guān)系主要考dict葛菇,并且dict的查找復(fù)雜度是O(1)而skiplist的查找復(fù)雜度為O(lgn),所以zsetScore的實(shí)現(xiàn)就是通過dictFind來實(shí)現(xiàn)橡羞。

修改redis-cli.c

下面我們來看看對于源碼redis-cli.c我們是如何做修改的:

首先是對typeinfo結(jié)構(gòu)體的修改:

//old
typedef struct {
    char *name;
    char *sizecmd;
    char *sizeunit;
    unsigned long long biggest;
    unsigned long long count;
    unsigned long long totalsize;
    sds biggest_key;
} typeinfo;
typeinfo type_string = { "string", "STRLEN", "bytes"};
typeinfo type_list = { "list", "LLEN", "items"};
typeinfo type_set = { "set", "SCARD", "members"};
typeinfo type_hash = { "hash", "HLEN", "fields"};
typeinfo type_zset = { "zset", "ZCARD", "members"};
typeinfo type_stream = { "stream", "XLEN", "entries"};
typeinfo type_other = { "other", NULL, "?" };

//new
typedef struct {
    char *name;
    char *sizecmd;
    char *sizeunit;
    int i_name;//數(shù)據(jù)類型(int)
    unsigned long long count;
    unsigned long long totalsize;
    zset *bigkeys;
} typeinfo;
typeinfo type_string = { "string", "STRLEN", "bytes", BIT_STRING};
typeinfo type_list = { "list", "LLEN", "items", BIT_LIST};
typeinfo type_set = { "set", "SCARD", "members", BIT_SET};
typeinfo type_hash = { "hash", "HLEN", "fields", BIT_HASH};
typeinfo type_zset = { "zset", "ZCARD", "members", BIT_ZSET};
typeinfo type_stream = { "stream", "XLEN", "entries", BIT_STREAM};
typeinfo type_other = { "other", NULL, "?" ,BIT_OTHER};

舊的typeinfo只保存了biggest key的鍵名和大小眯停,新的則將其刪除,并增添一個(gè)zset指針來存儲(chǔ)多個(gè)bigkey卿泽。其次還新增了int型的i_name變量莺债,name是用字符串來表示該數(shù)據(jù)類型,而i_name則是用整數(shù)表示該數(shù)據(jù)類型签夭,在后續(xù)查詢對應(yīng)數(shù)據(jù)類型配置信息時(shí)會(huì)用到齐邦。type_xxx常量的值也進(jìn)行了改變,新增了BIT_XXX等值第租,從BIT_STRINGBIT_OTHER的是0~6措拇。

其次,第一版程序支持對所有6種數(shù)據(jù)類型有以下功能:是否掃描該數(shù)據(jù)類型慎宾、輸出最多多少個(gè)bigkey丐吓、bigkey閾值是啥三個(gè)功能浅悉。我定義了一個(gè)bigkeyConfig_t這種數(shù)據(jù)結(jié)構(gòu)在zset.h中:

typedef struct bigkeyConfig_t{
    uint64_t output_num;
    uint32_t thro_size;
    int need_scan;
}bigkeyConfig_t;

為了做到風(fēng)格統(tǒng)一,因?yàn)閞edis服務(wù)器所有的配置信息都放在全局變量config中券犁,所以我也將bigkeyConfig_t變量也放在config全局變量中:

static struct config {
    char *hostip;
    int hostport;
    char *hostsocket;
    ......
    //redis-bigkey-online
    FILE *bk_pFile;//輸出位置
    bigkeyConfig_t *bk_config;//配置信息
} config;

我新增了兩個(gè)變量放在config的末尾术健,bk_pFile是文件指針,表示用戶想將程序結(jié)果輸出在標(biāo)準(zhǔn)輸出中還是文件中粘衬,這個(gè)可在配置文件bigkeys.conf進(jìn)行設(shè)置荞估;bk_configbigkeyConfig_t*類型的指針,指向6個(gè)bigkeyConfig_t結(jié)構(gòu)體稚新,每一個(gè)結(jié)構(gòu)體都表示對應(yīng)一種數(shù)據(jù)類型的配置信息勘伺。

以上便是所有結(jié)構(gòu)體的改動(dòng),下面我們跟著服務(wù)器啟動(dòng)的順序來看下如何發(fā)揮作用:

1.main函數(shù)開頭枷莉,對config進(jìn)行默認(rèn)初始化
2.main中娇昙,執(zhí)行parseOptions對命令行參數(shù)進(jìn)行解析
3.parseOptions中,執(zhí)行l(wèi)oadBigKeyConfig對用戶配置文件進(jìn)行解析
3.回到main笤妙,執(zhí)行findBigKeys開始找bigkeys
  1. main函數(shù)入口冒掌,對config全局變量進(jìn)行默認(rèn)初始化:

    int main(int argc, char **argv) {
        int firstarg;
    
        //redis-bigkey-online default config
        config.bk_pFile = stdout;
        config.bk_config = NULL;
    
        config.hostip = sdsnew("127.0.0.1");
        config.hostport = 6379;
        config.hostsocket = NULL;
        config.repeat = 1;
        config.interval = 0;
        config.dbnum = 0;
        ......
    

    同時(shí)我們也為新增的域進(jìn)行了默認(rèn)設(shè)置,文件輸出位置默認(rèn)為stdout蹲盘,配置信息指向NULL股毫。

  2. 接著程序?qū)edis-cli的命令行參數(shù)進(jìn)行配置:

    firstarg = parseOptions(argc,argv);
    
    static int parseOptions(int argc, char **argv) {
        int i;
    
        for (i = 1; i < argc; i++) {
            int lastarg = i==argc-1;
    
            if (!strcmp(argv[i],"-h") && !lastarg) {
                sdsfree(config.hostip);
                config.hostip = sdsnew(argv[++i]);
            } else if (!strcmp(argv[i],"-h") && lastarg) {
                usage();
            } else if (!strcmp(argv[i],"--help")) {
                usage();
            } else if (!strcmp(argv[i],"-x")) {
                config.stdinarg = 1;
            } else if (!strcmp(argv[i],"-p") && !lastarg) {
                config.hostport = atoi(argv[++i]);
            } else if (!strcmp(argv[i],"-s") && !lastarg) {
                config.hostsocket = argv[++i];
            }
            ......
            else if (!strcmp(argv[i],"--bigkeys")) {
                config.bigkeys = 1;
                loadBigKeyConfig(argv[++i],0);
            }
    

    parseOptions函數(shù)會(huì)對命令行參數(shù)進(jìn)行解析,如用戶輸入redis-cli -h 127.0.0.1 -p 6379時(shí)則會(huì)對config中的地址和端口進(jìn)行賦值召衔。當(dāng)程序識別到用戶輸入--bigkeys選項(xiàng)時(shí)铃诬,會(huì)讓config.bigkeys標(biāo)志位為1,注意此標(biāo)志位是系統(tǒng)本來就有的苍凛,不是我新增的趣席。我新增的是后面的loadBigKeyConfig()函數(shù)。舊的--bigkeys選項(xiàng)是沒有后續(xù)參數(shù)的醇蝴,因?yàn)槲倚略隽苏襜igkey的配置文件宣肚,需要用戶從redis-cli --bigkeys變?yōu)?code>redis-cli --bigkeys bigkeys,conf,所以loadBigKeyConfig(argv[++i],0)就是加載后續(xù)參數(shù)對應(yīng)的配置文件并進(jìn)行解析:

  3. 對用戶設(shè)置的配置文件進(jìn)行解析:

    loadBigKeyConfig()函數(shù)是參照了server.c中的loadServerConfig()函數(shù)悠栓。首先給config.bk_config分配6個(gè)結(jié)構(gòu)體大小的內(nèi)存霉涨,然后打開配置文件,如果打開文件成功就將配置文件的所有內(nèi)容一行一行地追加到字符串變量config_str當(dāng)中:

    void loadBigKeyConfig(const char *filename,int memkeys){
        sds config_str = sdsempty();
        char buf[CONFIG_MAX_LINE+1];
        char *err = NULL;
        int linenum = 0, totlines, i;
        long int config_val;
        sds *lines;
    
        config.bk_config = zmalloc(6*sizeof(bigkeyConfig_t));
    
        /* Load the file content */
        if (filename) {
            FILE *fp;
    
            if ((fp = fopen(filename,"r")) == NULL) {
                printf("Fatal error, can't open config file '%s': %s",
                    filename, strerror(errno));
                exit(1);
            }
    
            while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
                config_str = sdscat(config_str,buf);
            fclose(fp);
        }
        ...... 
    }
    

    當(dāng)配置文件全部追加到config_str變量后惭适,調(diào)用sdssplitlen()函數(shù)將config_str以換行符為界進(jìn)行切割笙瑟,將各行依次存入lines字符串?dāng)?shù)組中。緊接著就是對每行內(nèi)容進(jìn)行處理癞志,包括跳過空行往枷、檢查配置信息格式是否正確、將正確配置信息存入config.bk_config中等等:

    void loadBigKeyConfig(const char *filename,int memkeys){
        ......
     lines = sdssplitlen(config_str,strlen(config_str),"\n",1,&totlines);
    
        for(i=0;i<totlines;++i){
            sds *argv;
            int argc;
    
            linenum = i+1;
            lines[i] = sdstrim(lines[i]," \t\r\n");
    
            /* Skip comments and blank lines */
            if (lines[i][0] == '#' || lines[i][0] == '\0') continue;
    
            /* Split into arguments */
            argv = sdssplitargs(lines[i],&argc);
            if (argv == NULL) {
                err = "Unbalanced quotes in configuration line";
                goto loaderr;
            }
    
             ......
                
            }
            sdsfreesplitres(argv,argc);
        }
        sdsfreesplitres(lines,totlines);
        sdsfree(config_str);
        return;
    }
    

    加載、解析完用戶的配置文件后错洁,便可以繼續(xù)往下走了茅信。

  4. 執(zhí)行findBigKeys()函數(shù)

    當(dāng)用戶的配置文件解析完(loadBigKeyConfig)回到redis-cli的解析命令行參數(shù)函數(shù)中(parseOptions),當(dāng)所有命令行參數(shù)都解析完后就回到主函數(shù)中(main)繼續(xù)向下運(yùn)行:

    int main(int argc, char **argv) {
        ......
        /* Find big keys */
        if (config.bigkeys) {
            if (cliConnect(0) == REDIS_ERR) exit(1);
            findBigKeys(0, 0);
        }
        ......
    }
    

    如果config.bigkeys標(biāo)志位被設(shè)置了墓臭,那就執(zhí)行findBigKeys函數(shù)。

  5. findBigKeys()具體流程

    此函數(shù)最開頭已經(jīng)分析過了妖谴,這里只講變化的部分,首先是所有的printf函數(shù)變成fprintf函數(shù)窿锉,根據(jù)config.bk_pFile的值決定輸出位置,如:

    //old
    /* Status message */
    printf("\n# Scanning the entire keyspace to find biggest keys as well as\n");
    printf("# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec\n");
    printf("# per 100 SCAN commands (not usually needed).\n\n");
    
    //new
    /* Status message */
    fprintf(config.bk_pFile,"\n# Scanning the entire keyspace to find biggest keys as well as\n");
    fprintf(config.bk_pFile,"# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec\n");
    fprintf(config.bk_pFile,"# per 100 SCAN commands (not usually needed).\n\n");
    

    其次就是判斷bigkey及其處理過程膝舅,舊程序是用typeinfo結(jié)構(gòu)體記錄的最大key和目前正在遍歷的key作比較嗡载,目前遍歷到的key更大的話就替換typeinfo結(jié)構(gòu)體里面原本的最大key信息(biggest和biggest_key)。新版代碼會(huì)先判斷config.bk_config的配置信息仍稀,看該類型的key是否需要記錄洼滚,不需要直接跳過。接著判斷該key是否大于該類型的閾值技潘,大于的話只能說明它是個(gè)bigkey遥巴,但是還要進(jìn)一步判斷是否超過了我們需要的bigkey數(shù)量夫啊。如果數(shù)量還沒到上限則直接將該bigkey插入typeinfo結(jié)構(gòu)體的zset里面体斩,如果達(dá)到上限的話和zset的最小值進(jìn)行比較,大于最小值就先刪除最小值再將此key插入讯檐,如果小于最小值那就直接舍棄此key:

    //old
     if(type->biggest<sizes[i]) {
         /* Keep track of biggest key name for this type */
         if (type->biggest_key)
             sdsfree(type->biggest_key);
         type->biggest_key = sdscatrepr(sdsempty(), keys->element[i]->str, keys->element[i]->len);
         if(!type->biggest_key) {
             fprintf(stderr, "Failed to allocate memory for key!\n");
             exit(1);
         }
    
         /* Keep track of the biggest size for this type */
         type->biggest = sizes[i];
     }
    
    //new
    //如果不是所需要輸出的類型值桩,跳過分析
    if(!config.bk_config[type->i_name].need_scan)
        continue;
    //如果key大于對應(yīng)類型的閾值
    if(sizes[i] >= config.bk_config[type->i_name].thro_size) {
        sds keyname = sdscatrepr(sdsempty(), keys->element[i]->str, keys->element[i]->len);
        if(!keyname) {
            fprintf(stderr, "Failed to allocate memory for key!\n");
            exit(1);
        }
        //統(tǒng)計(jì)的大key數(shù)量還沒到上限
        if(zsetLength(type->bigkeys) < config.bk_config[type->i_name].output_num){
            zsetAdd(type->bigkeys,sizes[i],keyname);
        }else{
            double score;
            sds min_key = zsetMin(type->bigkeys);
            zsetScore(type->bigkeys,min_key,&score);
            //如果key的大小大于已記錄的大key的最小值
            if(sizes[i] > (unsigned long long)score){
                zsetDel(type->bigkeys,min_key);
                zsetAdd(type->bigkeys,sizes[i],keyname);
            }
        }
    
        sdsfree(keyname);
    }
    

    然后就是輸出統(tǒng)計(jì)信息摆霉,輸出完后釋放各種用到的結(jié)構(gòu)體內(nèi)存然后回到main函數(shù)。以上就是整個(gè)解析流程了奔坟。

性能比較

這里比較解析能力携栋,就把bigkey閾值設(shè)為0,輸出數(shù)量也設(shè)為無上限咳秉,并且全部數(shù)據(jù)類型都要解析婉支。事先通過腳本向redis服務(wù)中string、list滴某、set磅摹、zset、hash中各插入10000個(gè)normalkey和2兩個(gè)bigkey霎奢,stream類型不插入數(shù)據(jù)户誓。并且通過/usr/bin/time -v獲取進(jìn)程執(zhí)行時(shí)間、cpu利用率等信息幕侠。

redis-bigkey-online

可以看到用戶運(yùn)行時(shí)間為0.24秒帝美,系統(tǒng)運(yùn)行時(shí)間為0.11秒,cpu占用率為58%晤硕,最大占用內(nèi)存為6392字節(jié)悼潭。

image-20210202145937521.png

python腳本

import sys
import redis

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print('Usage: python ', sys.argv[0], ' host port outputfile ')
        exit(1)
    host = sys.argv[1]
    port = sys.argv[2]
    outputfile = sys.argv[3]
    r = redis.StrictRedis(host=host, port=int(port))
    f = open(outputfile, "w")

    for k in r.scan_iter():
        length = 0
        try:
            type = r.type(k)
            if type == b'string':
                length = r.strlen(k)
            elif type == b'hash':
                length = r.hlen(k)
            elif type == b'list':
                length = r.llen(k)
            elif type == b'set':
                length = r.scard(k)
            elif type == b'zset':
                length = r.zcard(k)
            elif type == b'stream':
                length = r.xlen(k)
        except:
            sys.exit(1)
        if length > 0:
            print(k, type, length, file=f)

雖然代碼足夠精簡庇忌,但是可以看到用戶運(yùn)行時(shí)間為4.99秒,系統(tǒng)運(yùn)行時(shí)間為1.27秒舰褪,cpu占用率為79%皆疹,最大占用內(nèi)存為13060字節(jié)

image-20210202144859473.png

redis-rdb-tools(已安裝python-lzf)

redis-rdb-tools是github非常受歡迎的一款分析rdb文件的工具占拍,有4k+的star數(shù)略就。并且由于其是離線方式分析redis的持久化文件,避免了客戶端命令查詢的網(wǎng)絡(luò)IO消耗晃酒,理論上速度是快于腳本的表牢。redis-rdb-tools的-c justkeys選項(xiàng)是其最快的解析命令,只輸出鍵名不輸出其他信息贝次,下面為測試結(jié)果:

慘不忍睹崔兴!可以看到用戶運(yùn)行時(shí)間為18.55秒,系統(tǒng)運(yùn)行時(shí)間為0.16秒蛔翅,cpu占用率為99%敲茄,最大占用內(nèi)存為60548字節(jié)。由于redis-rdb-tools實(shí)現(xiàn)的功能過于冗雜繁多搁宾,所以反而導(dǎo)致其速度遠(yuǎn)低于存python腳本折汞。

image-20210202152132722.png

常見問題

  1. 你的項(xiàng)目這么好,有什么缺陷嗎盖腿?

    這個(gè)項(xiàng)目和所有在線腳本一樣爽待,因?yàn)?code>--bigkeys選項(xiàng)的源碼本質(zhì)就是客戶端不斷發(fā)送命令給服務(wù)器進(jìn)行查詢信息實(shí)現(xiàn)的,所以盡量避免在遠(yuǎn)程的客戶端運(yùn)行該選項(xiàng)翩腐,盡量在服務(wù)器本地執(zhí)行程序

  2. 為什么不實(shí)現(xiàn)輸出bigkey時(shí)同時(shí)將該key屬于哪個(gè)數(shù)據(jù)庫的信息也輸出鸟款?

    這里不是沒想到,是沒必要茂卦。因?yàn)閞edis-cli本身就實(shí)現(xiàn)了這個(gè)功能何什。我們加入我們想找3號數(shù)據(jù)庫的bigkey,就使用

    ./redis-cli -h 127.0.0.1 -p 6379 -n 3 --bigkeys bigkeys.conf
    

    如果不輸入-n選項(xiàng)就是默認(rèn)連接0號數(shù)據(jù)庫等龙。這樣還有個(gè)好處就是你可以建立一個(gè)腳本開多線程处渣,每個(gè)線程分析一個(gè)數(shù)據(jù)庫,這樣可以最大限度地利用CPU資源蛛砰。

  3. 為什么不實(shí)現(xiàn)輸出bigkey時(shí)同時(shí)將該key的expire(過期時(shí)間)信息也輸出罐栈?

    后續(xù)版本支持。

  4. 你為啥不也去實(shí)現(xiàn)個(gè)rdb版本的bigkey查找程序泥畅?

    然而事實(shí)是我之前實(shí)現(xiàn)過荠诬,在之前實(shí)習(xí)期間mentor就叫我實(shí)現(xiàn)個(gè)找bigkey的程序。當(dāng)時(shí)就是深入了解redis源碼后用純C實(shí)現(xiàn)了redis-rdb-bigkey項(xiàng)目,性能上也是吊打redis-rdb-tools柑贞。而這次修改源碼的動(dòng)力之一也是我曾經(jīng)做過的redis-rdb-bigkey項(xiàng)目方椎。

  5. 通過命令查詢的方式有個(gè)缺陷就是只知道比如hash的field數(shù)量是多少而不能確定整個(gè)hash數(shù)據(jù)占用的內(nèi)存是多少!

    淦钧嘶!就等你問這句話了L闹凇!有决! 確實(shí)拿hash來說摄欲,field數(shù)量多不代表它占用的內(nèi)存就大,field數(shù)量少也不一定代表它占用內(nèi)存就小疮薇,比如一個(gè)hash只有兩個(gè)field,但是每個(gè)field大小有一個(gè)G我注!這無疑是一個(gè)bigkey按咒,所以只通過HLEN命令獲取它的field數(shù)量來判斷是不是bigkey很偏頗。

    但是如果你仔細(xì)看findBigKeys(int memkeys, unsigned memkeys_samples)會(huì)發(fā)現(xiàn)它有兩個(gè)參數(shù)memkeys但骨、memkeys_samples励七,這兩個(gè)參數(shù)是和--memkeys選項(xiàng)有關(guān)的,如果你運(yùn)行的時(shí)--memkey的話奔缠,那么memkeys的值就為1掠抬,那findBigKeys()函數(shù)查詢單個(gè)key的命令就變成了MEMORY USAGE {keyname},從而可以獲得每一個(gè)key的實(shí)際內(nèi)存占用大行0ァ两波!對源程序稍加改變就可以實(shí)現(xiàn)--memkeys選項(xiàng)的個(gè)性化使用,現(xiàn)版本已支持如下命令:

    ./redis-cli -h 127.0.0.1 -p 6379 --memkeys memkeys.conf
    

    memkeys.confbigkeys.conf唯一不同的就是xx_thro_size都變成了帶單位的閾值闷哆,比如hash_thro_size 30KB腰奋。以下是一次運(yùn)行結(jié)果:

image-20210202161737433.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市抱怔,隨后出現(xiàn)的幾起案子劣坊,更是在濱河造成了極大的恐慌,老刑警劉巖屈留,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件局冰,死亡現(xiàn)場離奇詭異,居然都是意外死亡灌危,警方通過查閱死者的電腦和手機(jī)康二,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乍狐,“玉大人赠摇,你說我怎么就攤上這事。” “怎么了藕帜?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵烫罩,是天一觀的道長。 經(jīng)常有香客問我洽故,道長贝攒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任时甚,我火速辦了婚禮隘弊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荒适。我一直安慰自己梨熙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布刀诬。 她就那樣靜靜地躺著咽扇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陕壹。 梳的紋絲不亂的頭發(fā)上质欲,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音糠馆,去河邊找鬼嘶伟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛又碌,可吹牛的內(nèi)容都是我干的九昧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼毕匀,長吁一口氣:“原來是場噩夢啊……” “哼耽装!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起期揪,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤掉奄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后凤薛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姓建,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年缤苫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了速兔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡活玲,死狀恐怖涣狗,靈堂內(nèi)的尸體忽然破棺而出谍婉,到底是詐尸還是另有隱情,我是刑警寧澤镀钓,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布穗熬,位于F島的核電站,受9級特大地震影響丁溅,放射性物質(zhì)發(fā)生泄漏唤蔗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一窟赏、第九天 我趴在偏房一處隱蔽的房頂上張望妓柜。 院中可真熱鬧,春花似錦涯穷、人聲如沸棍掐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塌衰。三九已至,卻和暖如春蝠嘉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杯巨。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工蚤告, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人服爷。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓杜恰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仍源。 傳聞我的和親對象是個(gè)殘疾皇子心褐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容