设为首页 收藏本站
查看: 1532|回复: 0

[经验分享] Redis源码分析:初步分析get命令

[复制链接]

尚未签到

发表于 2016-12-20 08:03:39 | 显示全部楼层 |阅读模式
Redis源码分析:初步分析get命令(一)

文章允许非商业性质转载,但请标明出处:http://forenroll.iyunv.com/
我刚刚才开始适应在Linux环境下工作,本来想用codeblocks来看代码,但是用它却不能调试,每次我一执行build就出错,于是我就用vim看代码,添加“调试信息”,然后make clean && make。整个过程就是简单粗暴。而且我的C语言基础基本忘光了,在后面调试的时候遇到很多问题都没解决。下面的代码我都尽量贴的原始的,只是为了篇幅删了一些注释,避免用我的“调试信息”污染大家的眼睛。如果谁有好的调试方法或工具还希望分享一下。
要了解get命令的整个处理过程,必须了解Redis中的各种数据结构。下面是在get命令的处理过程中会用到的一些数据结构:
redisClient
redisServer
redisObject
下面是我自己写的一个函数,用来打印redisObject。但只能打印类型是字符串,编码是字符串或整型的对象。因为我还没弄清楚其他的类型和编码是怎么存储,结构是什么样的。

/*                                                                             
* ===  FUNCTION  ==================================================================
*         Name:  printRedisObject                                                
*  Description:  added by forenroll                                                     
*  file position:src/redis.c                                                      
* =================================================================================
*/                                                                                
void printRedisObject (robj *o )                                                   
{                                                                                 
unsigned type = o->type;                                                      
unsigned encoding = o->encoding;                                               
if(type!=REDIS_STRING)                                                         
{                                                                              
printf("the robj is not string type, could not print it now\n");           
return;                                                                    
}                                                                              
switch(encoding){                                                              
case REDIS_ENCODING_RAW:                                                   
printf("the robj is string type, and its value is %s, and its length is %d\n",o->ptr,sdslen(o->ptr));
break;                                                                 
case REDIS_ENCODING_INT:                                                   
printf("the robj is long type, and its value is %ld\n",(long)o->ptr);
break;                                                                 
default:                                                                  
printf("could not print the robj\n");                                 
break;                                                                 
}                                                                              
return;                                                                        
}       /* -----  end of function printRedisObject  ----- */  

get命令的实现函数非常简单,它调用了getGenericCommand函数:

void getCommand(redisClient *c) {                                                
getGenericCommand(c);                                                         
}  

getGenericCommand函数的内容也比较简单:

int getGenericCommand(redisClient *c) {                                                                                 
robj *o;                                                                  
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)      
{                                                                          
return REDIS_OK;                                                      
}                                                                          
if (o->type != REDIS_STRING) {                                             
addReply(c,shared.wrongtypeerr);                                       
return REDIS_ERR;                                                      
} else {                                                                  
addReplyBulk(c,o);                                                     
return REDIS_OK;                                                      
}                                                                          
}  

这两个函数都在src/t_strint.c文件中定义。
(src/db.c)lookupKeyReadOrReply函数根据key去db。这个函数的名字也比较奇怪,lookupKeyRead or Reply,下面是它的实现:

robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {                                                                                                   
robj *o = lookupKeyRead(c->db, key);                                          
if (!o) addReply(c,reply);                                                   
return o;                                                                     
}   

从实现上看,只有找不到key的时候才会reply。这个地方我有一个疑问:为什么查询的方法还要负责reply。而不是直接在getGenericCommand函数里调用lookupKeyRead,或者不管怎么样这个函数就不应该加一个reply的职责。这种设置可能是由于某种原因,因为我还不够深入才未发现。
get命令取回的值都是字符串类型的,所以getGenericCommand函数中走的是else语句。那我们来看看(src/networking.c)addReplyBulk函数都做了些什么:

  /* Add a Redis Object as a bulk reply */                                                                                                                          
void addReplyBulk(redisClient *c, robj *obj) {                                 
addReplyBulkLen(c,obj);                                                   
addReply(c,obj);                                                           
addReply(c,shared.crlf);                                                   
}

(src/networking.c)addReplyBulkLen告诉客户端,接下来的消息的长度,第一个(src/networking.c)addReply发送真正的消息,第二个addReply发送的是一个"\r\n",这应该是一个消息的结束标志。 addReplyBulkLen函数:

/* Create the length prefix of a bulk reply, example: $2234 */                 
void addReplyBulkLen(redisClient *c, robj *obj) {                                                               
size_t len;                                                               
printRedisObject(obj);                                                     
if (obj->encoding == REDIS_ENCODING_RAW) {                                 
len = sdslen(obj->ptr);                                                
} else {                                                                       
long n = (long)obj->ptr;                                                   
/* Compute how many bytes will take this integer as a radix 10 string */
len = 1;                                                                  
if (n < 0) {                                                               
len++;                                                                 
n = -n;                                                               
}                                                                          
while((n = n/10) != 0) {                                                   
len++;                                                                 
}                                                                          
}                                                                              
addReplyLongLongWithPrefix(c,len,'$');                                         
}         

这个函数就是用来计算消息的长度,并且将长度以”$n\r\n”的形式返回给客户端的。如果消息长度为4,则返回”$4\r\n”。addReplyBulkLen这个函数貌似只在get命令中用来计算消息长度,其他命令可能也有相应的计算函数。注意:在第一个addReply函数中并没有在消息后面加上”\r\n”。



Redis源码分析:初步分析get命令(二)

redisDb类型的定义:

typedef struct redisDb {                                                                                                                                               
dict *dict;                 /* The keyspace for this DB */                    
dict *expires;              /* Timeout of keys with a timeout set */         
dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */  
dict *ready_keys;           /* Blocked keys that received a PUSH */           
dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */            
int id;                                                                       
long long avg_ttl;          /* Average TTL, just for stats */                 
} redisDb;   

redisDb类型中有两个field是与本次分析相关的,dict、expires,这两个变量分别存储所有的key-value和key-expiretime。所有的key-value都存储在dict这个“字典”里,而expires里则存储了所有设置有过期时间的key和它的过期时间。

robj *lookupKeyRead(redisDb *db, robj *key) {                                                                                                                          
robj *val;                                                                    
expireIfNeeded(db,key);                                                      
val = lookupKey(db,key);                                                      
if (val == NULL)                                                              
server.stat_keyspace_misses++;                                            
else                                                                          
server.stat_keyspace_hits++;                                             
return val;                                                                  
}   

(src/db.c)lookupKeyReadOrReply函数直接调用(src/db.c)lookupKeyRead函数。程序会在lookupKeyRead函数中通过调用函数(src/db.c)expireIfNeeded判断key的过期时间,如果过期则将其设置为过期,如果日志开启就将过期信息写入日志,并告知所有的slave。

int expireIfNeeded(redisDb *db, robj *key) {                                   
long long when = getExpire(db,key);//这个函数会查询db->expires里key的过期时间。                                         
if (when < 0) return 0; /* No expire for this key */                       
/* Don't expire anything while loading. It will be done later. */         
if (server.loading) return 0;                                             
if (server.masterhost != NULL) { //当前server是slave时,也同样对待                                          
return mstime() > when;                                                
}                                                                          
/* Return when this key has not expired */                                 
if (mstime() <= when) return 0;                                            
/* Delete the key */                                                      
server.stat_expiredkeys++;                                                
propagateExpire(db,key);//这个函数的作用是写日志和通知slave。                                 
return dbDelete(db,key);                                                                                 
}   

这里我又产生了一个疑惑:既然是优先执行了expire操作(注意expireIfNeeded在lookupKeyRead中的调用顺序),为什么不设置一个expire操作的返回值,告诉主程序(lookupKeyRead)库里存在这个key,但是刚刚检测到已经过期,并且已经删除了。那么后面就不用再去查询了。 可能这种设置有某种我还未了解到的原因吧。
lookupKeyRead调用了lookupKey,而lookupKey则直接调用了Redis里面的字典API:dictFind。 (src/dict.c)dictFind函数的实现:

dictEntry *dictFind(dict *d, const void *key)                                 
{                                                                              
dictEntry *he;                                                            
unsigned int h, idx, table;                                                
if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */   
if (dictIsRehashing(d)) _dictRehashStep(d);                                
h = dictHashKey(d, key);                                                   
for (table = 0; table <= 1; table++) {                                    
idx = h & d->ht[table].sizemask;                                       
he = d->ht[table].table[idx];                                                                                                                                 
while(he) {                                                               
if (dictCompareKeys(d, key, he->key))                                 
return he;                                                        
he = he->next;                                                        
}                                                                        
if (!dictIsRehashing(d)) return NULL;                                    
}                                                                             
return NULL;                                                                  
}

这个函数就是一个hash查找的过程,搞清楚了Redis的“字典”的数据结构,就可以搞清楚这个过程。在expireIfNeeded函数中调用的getExpire函数也调用这个API,因为key的过期时间也存储在相同的数据结构中。
我是一个Linux、C语言的新手。在看了这篇文章才开始翻看Redis的源代码,希望能学习更多的东西。在看源代码的过程发现,自己基础(Linux、c)差很多。这篇文章是作为自己的一个笔记,也有一点点思考的东西在里面,希望跟大家讨论,也希望大家能给出意见。谢谢。
     
Written with StackEdit.

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-316633-1-1.html 上篇帖子: redis之列表命令源码解析 下篇帖子: Redis的Aof被阻塞原因调查
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表