zoukankan      html  css  js  c++  java
  • Redis源码解析(1)

    在文章的开头我们把所有服务端文件列出来,并且标示出其作用:
    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 //
    各种对像的创建与销毁,stringlistsetzsethash
    rdb.c //redis
    数据文件处理
    redis.c //
    程序主要文件
    replication.c //
    数据同步master-slave
    sds.c //
    字符串处理
    sort.c //
    用于listsetzset排序
    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
    redisNDB(默认为16DB),并且每个db有一个hash表负责存放key,同一个DB不能有相同的KEY,但是不同的DB可以相同的KEY;
    2
    、支持的几种数据类型:stringhashlistsetzset;
    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 = sizeof(readonlyCommandTable)/sizeof(struct redisCommand);

    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), 该函数初始化一些服务器信息,包括创建事件处理对像、dbsocket、客户端链表、公共字符串等。
    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.ipfdserver.sofd有数据可读的情 况将会分别调用函数acceptTcpHandleracceptUnixHandler
    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.c250但是如果没有开启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里有两个命令BLPOPBRPOP需要使用这些操作(弹出列表头或者尾,实现方式见t_list.c:862行的 blockingPopGenericCommand函数),当指定的key不存在或者列表为空的情况下,那么客户端会一直阻塞,直到列表有数据时,服务 端就会去执行lpop或者rpop并返回给客户端,那么什么时候需要用到BLPOPBRPOP呢?大家平时肯定用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数量,内存使用量等);第二部分:判断DBhash 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,希望大家看文章的时候配合源代码一起看,更容易理解


    转自:http://blog.chinaunix.net/uid-790245-id-3766842.html

  • 相关阅读:
    SQL Server 索引的自动维护 <第十三篇>
    SQL Server 索引的图形界面操作 <第十二篇>
    python处理时间戳
    今天又犯了Java/Scala里面substring的错误
    新浪系统工程师笔试--shell
    把DEDE的在线文本编辑器换成Kindeditor不显示问题
    C语言 EOF是什么?
    Windows Server 2012 R2超级虚拟化之七 远程桌面服务的增强
    C++数据结构之最小生成树
    python sqlite 查询表的字段名 列名
  • 原文地址:https://www.cnblogs.com/freeopen/p/5482968.html
Copyright © 2011-2022 走看看