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

[经验分享] Redis源码简要分析

[复制链接]

尚未签到

发表于 2018-11-6 13:07:39 | 显示全部楼层 |阅读模式
  在文章的开头我们把所有服务端文件列出来,并且标示出其作用:
  adlist.c //双向链表
  ae.c //事件驱动
  ae_epoll.c //epoll接口, linux用
  ae_kqueue.c //kqueue接口, freebsd用
  ae_select.c //select接口, windows用
  anet.c //网络处理
  aof.c //处理AOF文件
  config.c //配置文件解析
  db.c //DB处理
  dict.c //hash表
  intset.c //转换为数字类型数据
  multi.c //事务,多条命令一起打包处理
  networking.c //读取、解析和处理客户端命令
  object.c //各种对像的创建与销毁,string、list、set、zset、hash
  rdb.c //redis数据文件处理
  redis.c //程序主要文件
  replication.c //数据同步master-slave
  sds.c //字符串处理
  sort.c //用于list、set、zset排序
  t_hash.c //hash类型处理
  t_list.c //list类型处理
  t_set.c //set类型处理
  t_string.c //string类型处理
  t_zset.c //zset类型处理
  ziplist.c //节省内存方式的list处理
  zipmap.c //节省内存方式的hash处理
  zmalloc.c //内存管理
  上面基本是redis最主要的处理文件,部分没有列出来,如VM之类的,就不在这里讲了。
  首先我们来回顾一下redis的一些基本知识:
  1、redis有N个DB(默认为16个DB),并且每个db有一个hash表负责存放key,同一个DB不能有相同的KEY,但是不同的DB可以相同的KEY;
  2、支持的几种数据类型:string、hash、list、set、zset;
  3、redis可以使用aof来保存写操作日志(也可以使用快照方式保存数据文件)
  对于数据类型在这里简单的介绍一下(网上有图,下面我贴上图片可能更容易理解)
  1、对于一个string对像,直接存储内容;
  2、对于一个hash对像,当成员数量少于512的时候使用zipmap(一种很省内存的方式实现hash table),反之使用hash表(key存储成员名,value存储成员数据);
  3、对于一个list对像,当成员数量少于512的时候使用ziplist(一种很省内存的方式实现list),反之使用双向链表(list);
  4、对于一个set对像,使用hash表(key存储数据,内容为空)
  5、对于一个zset对像,使用跳表(skip list),关于跳表的相关内容可以查看本blog的跳表学习笔记;

  下面正式进入源代码的分析
  1、首先是初始化配置,initServerConfig(redis.c:759)
  void initServerConfig() {
  server.port = REDIS_SERVERPORT;
  server.bindaddr = NULL;
  server.unixsocket = NULL;
  server.ipfd = -1;
  server.sofd = -1;
  2.在初始化配置中调用了populateCommandTable(redis.c:925)函数,该函数的目地是将命令集分布到一个hash table中,大家可以看到每一个命令都对应一个处理函数,因为redis支持的命令集还是蛮多,所以如果要靠if分支来做命令处理的话即繁琐效率还底, 因此放到hash table中,在理想的情况下只需一次就能定位命令的处理函数。
  void populateCommandTable(void) {
  int j;

  int numcommands =>  for (j = 0; j < numcommands; j++) {
  struct redisCommand *c = readonlyCommandTable+j;
  int retval;
  retval = dictAdd(server.commands, sdsnew(c->name), c);
  assert(retval == DICT_OK);
  }
  }
  3、对参数的解析,redis-server有一个参数(可以不需要),这个参数是指定配置文件路径,然后由函数loadServerConfig(config.c:28)加载所有配置
  if (argc == 2) {
  if (strcmp(argv[1], “-v”) == 0 ||
  strcmp(argv[1], “–version”) == 0) version();
  if (strcmp(argv[1], “–help”) == 0) usage();
  resetServerSaveParams();
  loadServerConfig(argv[1]);
  4、初始化服务器initServer(redis.c:836), 该函数初始化一些服务器信息,包括创建事件处理对像、db、socket、客户端链表、公共字符串等。
  void initServer() {
  int j;
  signal(SIGHUP, SIG_IGN);
  signal(SIGPIPE, SIG_IGN);
  setupSignalHandlers();//设置信号处理
  if (server.syslog_enabled) {
  openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
  server.syslog_facility);
  }
  5、在上面初始化服务器中有一段代码是创建事件驱动,aeCreateTimeEvent是创建一个定时器,下面创建的定时器将会每毫秒调用serverCron函数,而aeCreateFileEvent是创建网络事件驱动,当server.ipfd和server.sofd有数据可读的情 况将会分别调用函数acceptTcpHandler和acceptUnixHandler。
  aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
  if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
  acceptTcpHandler,NULL) == AE_ERR) oom(“creating file event”);
  if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
  acceptUnixHandler,NULL) == AE_ERR) oom(“creating file event”);
  6、接下来就是初始化数据,如果开启了AOF,那么会调用loadAppendOnlyFile(aof.c:216)去加载AOF文件,在AOF 文件中存放了客户端的命令,函数将数据读取出来然后依次去调用命令集去处理,当AOF文件很大的时候势必为影响客户端的请求,所以每处理1000条命令就 会去尝试接受和处理客户端的请求,其代码在aof.c第250行; 但是如果没有开启AOF并且有rdb的情况,会调用rdbLoad(redis.c:873)尝试去加载rdb文件,理所当然的在加载rdb文件的内部也 会考虑文件太大而影响客户端请求,所以跟AOF一样,每处理1000条也会尝试去接受和处理客户端请求。
  7、当所有初始化工作做完之后,服务端就开始正式工作了
  aeSetBeforeSleepProc(server.el,beforeSleep);
  aeMain(server.el);
  8、大家都知道redis是单线程模式,所有的请求、处理都是在同一个线程里面进行,也就是一个无限循环,在这个无限循环的内部有两件事要做,第一 件就是调用通过aeSetBeforeSleepProc函数设置的回调函数,第二件就是开始接受客户端的请求和处理,所以我们可以在第7节看到设置了回 调函数为beforeSleep,但是这个beforeSleep到底有什么作用呢?我们在第9节再详细讲述。对于aeMain(ae.c:375)就是 整个程序的主要循环。
  void aeMain(aeEventLoop *eventLoop) {
  eventLoop->stop = 0;
  while (!eventLoop->stop) {
  if (eventLoop->beforesleep != NULL)
  eventLoop->beforesleep(eventLoop);
  aeProcessEvents(eventLoop, AE_ALL_EVENTS);
  }
  }
  9、在beforeSleep内部一共有三部分,第一部分对vm进行处理(即第一个if块),这里我们略过;第二部分是释放客户端的阻塞操作,在redis里有两个命令BLPOP和BRPOP需要使用这些操作(弹出列表头或者尾,实现方式见t_list.c:862行的 blockingPopGenericCommand函数),当指定的key不存在或者列表为空的情况下,那么客户端会一直阻塞,直到列表有数据时,服务 端就会去执行lpop或者rpop并返回给客户端,那么什么时候需要用到BLPOP和BRPOP呢?大家平时肯定用redis做过队列,最常见的处理方式 就是使用llen去判断队列有没有数据,如果有数据就去取N条,然后处理,如果没有就sleep(3),然后继续循环,其实这里就可以使用BLPOP或者 BRPOP来轻松实现,而且可以减少请求,具体怎么实现留给大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),这个函 数主要目的是将aofbuf的数据写到文件,那aofbuf是什么呢?他是AOF的一个缓冲区,所以客户端的命令都会在处理完后把这些命令追加到这个缓冲 区中,然后待一轮数据处理完之后统一写到文件(所以aof也是不能100%保证数据不丢失的,因为如果当redis正在处理这些命令的情况下服务就挂掉, 那么这部分的数据是没有保存到硬盘的),大家都知道写数据到文件并不是立即写到硬盘,只是保存到一个文件缓冲区中,什么情况下会把缓冲区的数据转到硬盘 呢?只要满足如下三种条件的一种就能将数据真正存到硬盘:1、手动调用刷新缓冲区;2、缓冲区已满;3、程序正常退出。因此redis将数据写到文件缓冲 区之后会判断是否需要刷到硬盘,server.appendfsync有两种方式,第一种(APPENDFSYNC_ALWAYS):无条件刷新,即每次 写文件都会保存到硬盘,第二种(APPENDFSYNC_EVERYSEC):每隔一秒保存到硬盘。
  10、接下来我们开始讲解aeProcessEvents(ae.c:275)的处理流程,首先我们来回顾一下第5节设置的定时器和监听 socket事件处理,其中socket事件处理会回调acceptTcpHandler(networking.c:410)和定时器回调函数 serverCron(redis.c:519),在aeProcessEvents的内部有两部分需要处理,第一部分是调用aeApiPoll判断 socket是否有数据可读,整个服务端的socket里面要分监听socket和客户端socket,当有客户端链接服务器时,会触发监听socket 的事件处理函数,也就是acceptTcpHandler,而acceptTcpHandler会去调用createClient(networking.c:13)创建客户端对像,然后为这个客户端设置事件处理函数 readQueryFromClient(networking.c:827),所以当客户端有消息时就会触发客户端socket 事件处理函数,处理数据部分讲在后面详细讲解,接下来的第二部分就是定时器,每次在socket部分处理完后就用调用 processTimeEvents(ae.c:212)来处理定时器,那么内部实现也很简单,当设置定时器的时候就会计算好应该触发的时间,所以这里就 只需要判断当前时间是否大于或者等于应该触发的时间即可。那么这个定时器到底做了什么呢?请继续第11节。
  11、我们继续跟踪源代码serverCron(redis.c:519),整个函数分为七个部分,第一部分:在服务端打印一些关于DB的信息(包 括key数量,内存使用量等);第二部分:判断DB的hash table是否需要扩展大小tryResizeHashTables(redis.c:432);第三部分:关闭太长时间没有通信的链接 closeTimedoutClients(networking.c:629);第四部分:保存rdb文件 rdbSaveBackground(rdb.c:507),当然也是在需要保存的情况才会保存,即设置save参数;第五部分:清除过期的key,当然 这里不是清除全部,他只是随机取出一些activeExpireCycle(redic.c:477);第六部分:虚拟内存交换部分,将一部分key转到 虚拟内存中,这里的key也是随机抽取的, vmSwapOneObjectBlocking(vm.c:521);第七部分:主从同 步,replicationCron(replication.c:500)。
  12、在第10节中我们讲到客户端socket处理函数readQueryFromClient,这里我们一层层分析,首先是从客户端读取数据,然 后调用processInputBuffer,在内部先是判断类型,然后调用processInlineBuffer或者 processMultibulkBuffer解析参数,解析后的参数由argv存储参数,其类型是一个指向指针的指针,其中argv[0]是命令名称, 后面就是命令参数,argc存储参数数量;然后调用processCommand(redis.c:979)处理命令,在内部调用 lookupCommand(redis.c:940)获取命令对应的函数,然后调用freeMemoryIfNeeded(redis.c:1385) 判断是否需要释放一些内存,接下来就是调用call(redis.c:954)去执行命令,执行命令后会调用feedAppendOnlyFile(aof.c:137)把命令行保存到aofbuf中,然后判断是否需要同步数据到slave,如果需要则调用replicationFeedSlaves(replication.c:10),接下来就是判断是否需要将数据发送到监控端,如果需要则调用replicationFeedMonitors(replication.c:82),到这里整个服务流程就结束了。至于每条命令如何执行,大家可以去 查看以t_开头的几个文件。下面是一张整个服务的流程图。

  注:redis源代码为2.2.8,希望大家看文章的时候配合源代码一起看,更容易理解


运维网声明 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-631578-1-1.html 上篇帖子: redis主从安装部署 下篇帖子: redis读取字符串类型的整数时会直接将其当做整数来处理
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

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

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

扫描微信二维码查看详情

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


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


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


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



合作伙伴: 青云cloud

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