Redis源码系列之从何读起

看过很多资料介绍Redis源码的阅读方法,总结起来主流的大概有两种:

1、分模块章节一部分一部分的读,比如先阅读数据类型,再阅读相关处理机制。

2、由于Redis源码用C写成,直接从main()函数入手,逐步了解大概的处理框架和相关机制。类似于从主干到分枝的方法。

两种方法能汇总起来当然是最好,但花费精力巨大。读者可根据实际情况选择阅读方式。本系列将采用第二种方法简略记录阅读心得(源码版本选自5.0.4)。

 

上文提到Redis源码由C写成,可有main()函数入手,查看每一个命令输入之后执行经过的详细过程,这样我们可以从外部输入一步步了解内在逻辑。

比如一个ping命令从main()进入后到了processCommand过程 ,其中有声明命令表redisCommandTable,redisCommandTable表中有所有的Redis命令和相应的参数,

接着可以查看pingCommand命令,发现该命令十分简单,直接返回共享对象字符“pong”,接着我们可以去查看共享字符“pong”的定义方式。一个最简单的命令执行逻辑大概就清楚了。

我们还可以根据这个方法在Redis里实现程序世界里最简单的“hello,world”,在这里不做细述,感兴趣可以参考我的另一篇文章https://i.cnblogs.com/posts/edit-done;postId=14065547。

 

经常能听到说Redis是单线程模型(不考虑Redis6.0多线程IO),这话并不完全正确,如果看过源码,可以知道在主线程外还有多个后台线程(比如lazy-free和持久化子进程,详见bio.h和bio.c)。

主线程main函数的逻辑主要包括两个部分:

1、各种初始化,包括加载配置文件、初始化命令表、数据库服务等

2、执行事件循环

 

 

 我们通过源码细看各个环节的具体流程

1、配置加载和初始化:Redis会通过默认参数去初始化,并将其中已经修改掉的参数覆盖默认配置。其中涉及到的内容有用来加载配置参数的loadServerConfig函数、用来初始化所有Redis命令的populateCommandTable函数、初始化哨兵的initSentinelConfig函数等等。Redis服务是有redisServer的全局变量来表示的,变量里包含了上百个变量值和标识符,比如动态hz、配置文件等等。

2、创建事件循环。事件循环是由aeEventLoop来表示的,事件循环依赖操作系统底层的I/O多路复用机制。为什么Redis是单线程执行却能同时处理多个请求这是Redis相关工作人员无法回避的话题,在这个问题的答案中,I/O多路复用不可或缺,关于I/O多路复用,有个知乎回答我觉得讲的挺清楚,可以查看链接IO 多路复用是什么意思? - 罗志宇的回答 - 知乎 https://www.zhihu.com/question/32163005/answer/55772739

3、开始socket监听。Redis监听模式分为两种:TCP和IPC(Unix domain socket)。IPC是一种高效的进程间的通信机制,在同服务器中的不同进程有着更高的性能。而在一般的业务场景中,服务和数据库都部署在不同的服务器上,因此大部分业务都选择TCP。

4、注册时间事件回调。Redis作为一个单线程程序,如果想调用异步程序比如周期性的过期key(expire)、降低碎片率(defrag)、重置部分连接、渐进式rehash等等,除了依赖主事件循环没有其他的办法。在这个过程中会注册一个时间事件,形成周期性执行的回调函数ServerCron。它会在合适的时间调用(比如过期key超过了百分比阈值,aof文件大小触发了rewrite机制)

5、注册I/O事件回调。Redis服务端最主要的工作就是监听I/O事件,从中分析出来自客户端的命令请求,执行命令,然后返回响应结果。对于I/O事件的监听,自然也是依赖事件循环。前面提到过,Redis可以打开两种监听:对于TCP连接的监听和对于Unix domain socket的监听。因此,这里就包含对于这两种I/O事件的回调的注册,两个回调函数分别是acceptTcpHandleracceptUnixHandler。对于来自Redis客户端的请求的处理,就会走到这两个函数中去。我们在下一部分就会讨论到这个处理过程

6、初始化后台线程。Redis会创建一些额外的线程,在后台运行,专门用于处理一些耗时的并且可以被延迟执行的任务,比如删除大key(lazy free机制,aof重写等等)。

7、启动事件循环。这一步主要创建了一个死循环,可以保证前面注册的timer事件回调和I/O事件回调不断执行。

 

Redis处理命令的详细流程见下图:

 

 

整个过程主要有两步:

1、建立连接:客户端发起连接请求(通过TCP或Unix domain socket),服务器接收连接;
建立连接的逻辑:acceptTcpHandler-> acceptCommonHandler
acceptTcpHandler:在初始化时会创建I/O事件回调,将定时调用这个回调函数,用于接收client socket连接;
acceptCommonHandler:初始化客户端(createClient,初始化redisClient数据结构),判断连接数是否超出maxclients;

2、命令发送、执行和响应:客户端在新建连接上发送命令数据,服务器接收执行命令,并把结果返回客户端;为了在新的连接上能够接收到客户端发来的命令,需要在事件循环中为新建连接的文件描述符注册一个I/O事件回调:readQueryFromClient ,负责将从客户端读取的数据累积到查询缓冲区中,对应执行和相应。

从读取query buffer到处理命令的处理逻辑:readQueryFromClient-> processInputBufferAndReplicate-> processInputBuffer-> processCommand-> call
readQueryFromClient函数:读取客户端数据,存储到query buffer;
processInputBufferAndReplicate函数:processInputBuffer函数包装了一下,主要用于判断客户端是不是master客户端;
processInputBuffer函数:根据Redis协议解析客户端qeury buffer;
processCommand函数:搜索匹配命令表,做一些边界检查;
call函数:最终执行命令的函数;

 

总结一下,本文系统地记录了如下几个执行流程:

    1、从main函数启动后的初始化过程;

    2、事件循环的执行逻辑和原理;

    3、一个Redis命令从请求接收,到命令的解析和执行,再到执行结果返回的完整过程。

 

另有Redis命令调用关系图由于排版问题无法展示,有兴趣可小窗博主。

 

posted @ 2020-12-01 00:14  洲渚皓月掩映  阅读(136)  评论(0编辑  收藏  举报