一文讀懂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)之一驳阎。
其實(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ì)更新count
和totalsize
乔妈,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)圖:
這里可以很清楚地看到带欢,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)開始它們都是空的擂仍,如下圖所示:
getKeyTypes(types_dict, keys, types)
函數(shù)則是對keys
中的每個(gè)key,通過TYPE {keyname}
的形式獲取該key的類型并使types中的元素指向?qū)?yīng)的type_info
結(jié)構(gòu)體:
之后通過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é)果如下:
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)就行了!(?′?`?)
具體代碼
理想很豐滿沧竟,現(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.h
和dict.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.h
和skiplist.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.h
和zset.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_STRING
到BIT_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_config
為bigkeyConfig_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
-
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股毫。
-
接著程序?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)行解析: -
對用戶設(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ù)往下走了茅信。
-
執(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ù)。 -
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é)悼潭。
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é)。
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腳本折汞。
常見問題
-
你的項(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í)行程序
-
為什么不實(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資源蛛砰。 -
為什么不實(shí)現(xiàn)輸出bigkey時(shí)同時(shí)將該key的expire(過期時(shí)間)信息也輸出罐栈?
后續(xù)版本支持。
-
你為啥不也去實(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)目方椎。 -
通過命令查詢的方式有個(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.conf
和bigkeys.conf
唯一不同的就是xx_thro_size
都變成了帶單位的閾值闷哆,比如hash_thro_size 30KB
腰奋。以下是一次運(yùn)行結(jié)果: