问题:
redis可以设置一个策略,当内存达到一个给定的阈值的时候,按照给定的规则淘汰key,详情参考http://redis.io/topics/lru-cache。现在给redis增加一条命令,可以查询被淘汰的key,示例如下:
>evitedkeys
1)key1
2)key2
3)key3
...
注意:
redis版本是3.0.7
测试的环境是Ubuntu 14.04 LTS
思路:
第一步:给redis添加一条命令需要在哪些地方加代码?
第二步:找到根据某个策略删除key的代码位置,在删除那个key之前将它存到外存的一个文件中;
第三步:增加一条命令,读取外存中的那个文件获得被删除的keys,并按照需要的格式输出。
解决:
第一步:给redis添加一条命令需要在哪些地方加代码?
对于我这种完全没有事先看过redis源代码的人来说,一个部分一个部分地看、理解、然后读出程序结构、最后再找到加代码的地方,这种方法显然耗时太长。所以,可以换个思路:拿一条现有的命令,找到其实现方法,然后模仿着在对应的地方加上代码。于是我在redis整个源代码文件夹中搜索“sismember”命令,发现它出现在了help.h和redis.c中。help.h中的部分是帮助内容,可以忽略;redis.c中对应部分如下
{"sismember",sismemberCommand,3,"rF",0,NULL,1,1,1,0,0}
sismember是命令名,sismemberCommand是具体实现,3是参数的个数,“rF“关键字具体含义见代码上方的注释,后面几个数字表示第一个参数的位置,最后一个参数的位置,参数间的间隔等。
然后查找sismemberCommand,发现其还出现在了redis.h中:
void sismemberCommand(redisClient *c);
这里明显是sismemberCommand的声明,
以及t_set.c中:
void sismemberCommand(redisClient *c) {
robj *set;
if ((set = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,set,REDIS_SET)) return;
c->argv[2] = tryObjectEncoding(c->argv[2]);
if (setTypeIsMember(set,c->argv[2]))
addReply(c,shared.cone);
else
addReply(c,shared.czero);
}
这里明显是sismemberCommand的实现。
这下我们大致清楚了给redis增加一条命令所需要的步骤,然后模仿着:
在redis.c中,给结构体数组redisCommandTable[](这个就是命令表),根据相应的规则加上一个元素:
{"evitedkeys",evitedkeysCommand,1,"rF",0,NULL,0,0,0,0,0}
在redis.h中,我们声明这个函数:
void evitedkeysCommand(redisClient *c);
最后在redis.c中,实现这个函数(这里先是空实现,后面再加上具体的实现方法):
void evitedkeysCommand(redisClient *c){}
make之后,进入相应的redis-3.0.7目录,输入
src/redis-server
切换到另一个命令行,运行
redis-cli
输入
evitedkeys aa
得到的回复是
(error) ERR wrong number of arguments for 'evitedkeys' command
而不是(error) ERR unknown command "evitedkeys"。
这表明我们的命令已经添加成功了!
检验一下:
检验一下:
成功了!
这表明我们的命令已经添加成功了!
第二步;找到根据某个策略删除key的代码位置,在删除那个key之前将它存到外存的一个文件中。
如何在海量的代码中找到删除key的那个位置呢?搜索某一个策略“volatile-ttl”,并定位到外围的函数后,我们可以发现以下代码块
int freeMemoryIfNeeded(void) {
size_t mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves);
mstime_t latency, eviction_latency;
/* Remove the size of slaves output buffers and AOF buffer from the
* count of used memory. */
mem_used = zmalloc_used_memory();
......
/* volatile-random and allkeys-random policy */
....
/* volatile-lru and allkeys-lru policy */
....
/* Finally remove the selected key. */
....
}
根据注释和代码内容可以得知:freeMemoryIfNeed函数就是我们要找的函数。该函数先根据相应的策略(如allkeys-random,或volatile-ttl)选出需要删除的键(这里是用bestkey表示),然后再执行删除。
我们只需要在
/* Finally remove the selected key. */这个注释的前面,将bestkey存进外存中就行了,代码如下:
//在删除bestkey之前将它存进外存文件evitedkeysRecord中。由于之后在用fread的时候需要确定读取的字节数,所以这里也把bestkey的长度一起存起来
FILE* evitedkeysFile = fopen("evitedkeysRecord","ab+");
fseek(evitedkeysFile,0,SEEK_END);
int lengthOfEvitedkey = strlen(bestkey);
int *ptr = &lengthOfEvitedkey;
fwrite(ptr,sizeof(int),1,evitedkeysFile);
fwrite(bestkey,sizeof(char),strlen(bestkey)+1,evitedkeysFile);
fclose(evitedkeysFile);
检验一下:
编译运行之后,设定好maxmemory为一个不大不小的值(508000字节左右,太小的话一个key都无法添加,太大的话能够容纳的key太多,我手动添加耗时太长),设定maxmemory-policy为allkeys-random,手动添加多个keys,当发现有key已经被淘汰之后,打开redis-3.0.7文件夹下的evitedkeysRecord文件,发现里面记录着那些被淘汰了得key和key的长度。
这表明我们已经成功将淘汰了的keys存起来了!
第三步.增加一条命令,读取外存中的那个文件获得被删除的keys,并按照需要的格式输出。
读取文件中的keys很容易,使用fread即可;问题是如何按照
1)key1
2)key2
3)key3
这种格式输出?
思路是:找到一个命令,这个命令也会有这种输出多行结果的格式,然后阅读分析其代码实现,找到其中用于客户端输出的函数,最后在我们的命令中调用这个函数。
根据这个思路,我首先想到的是mget命令。在redisCommandTable[]中,它的代码实现是mgetCommand函数:
void mgetCommand(redisClient *c) {
int j;
addReplyMultiBulkLen(c,c->argc-1);
for (j = 1; j < c->argc; j++) {
robj *o = lookupKeyRead(c->db,c->argv[j]);
if (o == NULL) {
addReply(c,shared.nullbulk);
} else {
if (o->type != REDIS_STRING) {
addReply(c,shared.nullbulk);
} else {
addReplyBulk(c,o);
}
}
}
}
进一步深入,其中的addReplyMultiBulkLen,addReply,addReplyBulk函数的定义中,又会调用更多我不认识的函数,分析起来比较复杂。
换个思路:mgetCommand函数的实现之所以比较复杂,是因为在客户端输出之前,需要先查找数据库找到需要的key的value之后才输出。不如换一个命令:这条命令和mget,evitedkeys一样,有多行的输出,同时不需要查找数据库,实现起来足够简单,方便我们分析。根据这个思路,我想到的是config get命令:它查找的是redis.conf文件,这个文件的结构固定,所以这个函数的实现应该也足够简单。
void configGetCommand(redisClient *c) {
robj *o = c->argv[2];
void *replylen = addDeferredMultiBulkLength(c);
char *pattern = o->ptr;
char buf[128];
int matches = 0;
redisAssertWithInfo(c,o,sdsEncodedObject(o));
/* String values */
config_get_string_field("dbfilename",server.rdb_filename);
config_get_string_field("requirepass",server.requirepass);
config_get_string_field("masterauth",server.masterauth);
config_get_string_field("unixsocket",server.unixsocket);
config_get_string_field("logfile",server.logfile);
config_get_string_field("pidfile",server.pidfile);
......
setDeferredMultiBulkLength(c,replylen,matches*2);
}
其中的config_get_string_field函数其实是一个宏定义:
#define config_get_string_field(_name,_var) do { \
if (stringmatch(pattern,_name,0)) { \
addReplyBulkCString(c,_name); \
addReplyBulkCString(c,_var ? _var : ""); \
matches++; \
} \
} while(0);
看了上面两个代码块我们基本上能明白如何给客户端输出多行结果了:先用addDeferredMultibulkLength表示要输出多行命令,接着多次用addReplyBulkCString将每行输出的字符串放进缓存中,最后用setDeferredMultiBulkLength来发送输出缓存中的所有内容。于是evitedkeysCommand的实现代码如下:
void evitedkeysCommand(redisClient *c){
void *replylen = addDeferredMultiBulkLength(c);
unsigned long numOfEvitedkeys = 0;
FILE *readEvitedkeysFile = fopen("evitedkeysRecord","ab+");
fseek(readEvitedkeysFile,0,SEEK_SET);
int lengthOfKey;
while(fread(&lengthOfKey,sizeof(int),1,readEvitedkeysFile)){
//redis每个key的最大命名长度是1024字节。这里也可以用用malloc来动态分配内存。但是用malloc的话,编译的时候会报警
//如果用malloc下面一行代码就换成char *evitedkey = (char*)malloc(lengthOfKey*sizeof(char)+1);
char evitedkey[1025];
if(fread(evitedkey,sizeof(char),lengthOfKey*sizeof(char)+1,readEvitedkeysFile)){
addReplyBulkCString(c,evitedkey);
numOfEvitedkeys++;}
}
setDeferredMultiBulkLength(c,replylen,numOfEvitedkeys);
fclose(readEvitedkeysFile);
}
编译之后,进入redis-3.0.7目录,输入src/redis-server,然后进入另一个命令行,运行redis-cli,设置maxmemory为508500字节,maxmemory-policy为allkeys-random,手动依次添加key1,key2,key3,......,key15,结果如下:
成功了!