- [root@zbdba redis-3.0]# ./src/redis-cli -p 6379 -h 127.0.0.1 --stat
- ------- data ------ --------------------- load -------------------- - child -
- keys mem clients blocked requests connections
- 1 817.18K 2 0 1 (+0) 2
- 1 817.18K 2 0 2 (+1) 2
- 1 817.18K 2 0 3 (+1) 2
- 1 817.18K 2 0 4 (+1) 2
- 1 817.18K 2 0 5 (+1) 2
- 1 817.18K 2 0 6 (+1) 2
我们这里没有指定,就是默认的模式。
3、进入上图中 step1 的 cliConnect 方法,cliConnect 主要包含 redisConnect、redisConnectUnix 方法。这两个方法分别用于 TCP Socket 连接以及 Unix Socket 连接,Unix Socket 用于同一主机进程间的通信。我们上面是采用的 TCP Socket 连接方式也就是我们平常生产环境常用的方式,这里不讨论 Unix Socket 连接方式,如果要使用 Unix Socket 连接方式,需要配置 unixsocket 参数,并且按照下面方式进行连接:
- [root@zbdba redis-3.0]# ./src/redis-cli -s /tmp/redis.sock
- redis /tmp/redis.sock>
4、进入 redisContextInit 方法,redisContextInit 方法用于创建一个 Context 结构体保存在内存中,如下:
- /* Context for a connection to Redis */
- typedef struct redisContext {
- int err;
- /* Error flags, 0 when there is no error */
- char errstr[128];
- /* String representation of error when applicable */
- int fd;
- int flags;
- char * obuf;
- /* Write buffer */
- redisReader * reader;
- /* Protocol reader */
- }
- redisContext;
主要用于保存客户端的一些东西,最重要的就是 write buffer 和 redisReader,write buffer 用于保存客户端的写入,redisReader 用于保存协议解析器的一些状态。
5、进入 redisContextConnectTcp 方法,开始获取 IP 地址和端口用于建立连接,主要方法如下:
- s = socket(p->ai_family,p->ai_socktype,p->ai_protocol
- connect(s,p->ai_addr,p->ai_addrlen)
到此客户端向服务端发起建立 socket 连接,并且等待服务器端响应。
当然 cliConnect 方法中还会调用 cliAuth 方法用于权限验证、cliSelect 用于 db 选择,这里不着重讨论。
(点击放大图像)
服务器接收客户端的请求首先是从 epoll_wait 取出相关的事件,然后进入上图中 step2 中的方法,执行 acceptTcpHandler 或者 acceptUnixHandler 方法,那么这两个方法对应的事件是在什么时候注册的呢?他们是在服务器端初始化的时候创建。下面看看服务器端在初始化的时候与 socket 相关的地方
1、打开 TCP 监听端口
- if (server.port != 0 &&
- listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
- exit(1);
2、打开 unix 本地端口
- if (server.unixsocket != NULL) {
- unlink(server.unixsocket); /* don't care if this fails */
- server.sofd = anetUnixServer(server.neterr,server.unixsocket,
- server.unixsocketperm, server.tcp_backlog);
- if (server.sofd == ANET_ERR) {
- redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
- exit(1);
- }
- anetNonBlock(NULL,server.sofd);
- }
3、为 TCP 连接关联连接应答处理器 (accept)
- for (j = 0; j < server.ipfd_count; j++) {
- if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
- acceptTcpHandler,NULL) == AE_ERR)
- {
- redisPanic(
- "Unrecoverable error creating server.ipfd file event.");
- }
- }
4、为 Unix Socket 关联应答处理器
- if (server.sofd > 0 && aeCreateFileEvent
- (server.el,server.sofd,AE_READABLE,
- acceptUnixHandler,NULL) == AE_ERR)
- redisPanic("Unrecoverable error creating server.sofd file event.");
在 1/2 步骤涉及到的方法中是 Linux Socket 的常规操作,获取 IP 地址,端口。最终通过 socket、bind、listen 方法建立起 Socket 监听。也就是上图中 acceptTcpHandler 和 acceptUnixHandler 下面对应的方法。
在 3/4 步骤涉及到的方法中采用 aeCreateFileEvent 方法创建相关的连接应答处理器,在客户端请求连接的时候触发。
所以现在整个 socket 连接建立流程就比较清楚了,如下:
至此客户端和服务器端的 socket 连接已经建立,但是此时服务器端还继续做了 2 件事:
可以从图中得知,aeCreateFileEvent 调用 aeApiAddEvent 方法最终通过 epoll_ctl 方法进行注册事件。
(点击放大图像)
客户端在与服务器端建立好 socket 连接之后,开始执行上图中 step3 的 repl 方法。从图中可知 repl 方法接受输入输出主要是采用 linenoise 插件。当然这是针对 redis-cli 客户端哦。linenoise 是一款优秀的命令行编辑库,被广泛的运用在各种 DB 上,如 Redis、MongoDB,这里不详细讨论。客户端写入流程分为以下几步:
1、linenoise 等待接受用户输入
2、linenoise 将用户输入内容传入 cliSendCommand 方法,cliSendCommand 方法会判断命令是否为特殊命令,如:
客户端会根据以上命令设置对应的输出格式以及客户端的模式,因为这里我们是普通写入,所以不会涉及到以上的情况。
3、cliSendCommand 方法会调用 redisAppendCommandArgv 方法,redisAppendCommandArgv 方法会调用 redisFormatCommandArgv 和__redisAppendCommand 方法
redisFormatCommandArgv 方法用于将客户端输入的内容格式化成 redis 协议:
例如:
- set zbdba jingbo
- *3\r\n$3\r\n set\r\n $5\r\n zbdba\r\n $6\r\n jingbo
__redisAppendCommand 方法用于将命令写入到 outbuf 中
接着客户端进入下一个流程,将 outbuf 内容写入到套接字描述符上并传输到服务器端。
4、进入 redisGetReply 方法,该方法下主要有 redisGetReplyFromReader 和 redisBufferWrite 方法,redisGetReplyFromReader 主要用于读取挂起的回复,redisBufferWrite 方法用于将当前 outbuf 中的内容写入到套接字描述符中,并传输内容。
主要方法如下:
- nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
此时客户端等待服务器端接收写入。
(点击放大图像)
服务器端依然在进行事件循环,在客户端发来内容的时候触发,对应的文件读取事件。这就是之前创建 socket 连接的时候建立的事件,该事件绑定的方法是 readQueryFromClient 。此时进入 step4 的 readQueryFromClient 方法。
readQueryFromClient 方法用于读取客户端的发送的内容。它的执行步骤如下:
1、在 readQueryFromClient 方法中从服务器端套接字描述符中读取客户端的内容到服务器端初始化 client 的查询缓冲中,主要方法如下:
- nread = read(fd, c->querybuf+qblen, readlen);
2、交给 processInputBuffer 处理,processInputBuffer 主要包含两个方法,processInlineBuffer 和 processCommand。processInlineBuffer 方法用于采用 redis 协议解析客户端内容并生成对应的命令并传给 processCommand 方法,processCommand 方法则用于执行该命令
3、processCommand 方法会以下操作:
4、最后进入 call 方法。
call 方法会调用 setCommand,因为这里我们执行的 set zbdba jingbo,set 命令对应 setCommand 方法,redis 服务器端在开始初始化的时候就会初始化命令表,命令表如下:
- struct redisCommand redisCommandTable[] = {
- {
- "get",
- getCommand,
- 2,
- "r",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "set",
- setCommand,
- -3,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "setnx",
- setnxCommand,
- 3,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "setex",
- setexCommand,
- 4,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "psetex",
- psetexCommand,
- 4,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "append",
- appendCommand,
- 3,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "strlen",
- strlenCommand,
- 2,
- "r",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "del",
- delCommand,
- -2,
- "w",
- 0,
- NULL,
- 1,
- -1,
- 1,
- 0,
- 0
- },
- {
- "exists",
- existsCommand,
- 2,
- "r",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- {
- "setbit",
- setbitCommand,
- 4,
- "wm",
- 0,
- NULL,
- 1,
- 1,
- 1,
- 0,
- 0
- },
- ....
- }
所以如果是其他的命令会调用其他相对应的方法。call 方法还会做一些事件,比如发送命令到从库、发送命令到 aof、计算命令执行的时间。
5、setCommand 方法,setCommand 方法会调用 setGenericCommand 方法,该方法首先会判断该 key 是否已经过期,最后调用 setKey 方法。
这里需要说明一点的是,通过以上的分析。redis 的 key 过期包括主动检测以及被动监测
主动监测
被动监测
以上主要是让运维的同学更加清楚 redis 的 key 过期删除机制。
6、进入 setKey 方法,setKey 方法最终会调用 dbAdd 方法,其实最终就是将该键值对存入服务器端维护的一个字典中,该字典是在服务器初始化的时候创建,用于存储服务器的相关信息,其中包括各种数据类型的键值存储。完成了写入方法时候,此时服务器端会给客户端返回结果。
7、进入 prepareClientToWrite 方法然后通过调用_addReplyToBuffer 方法将返回结果写入到 outbuf 中(客户端连接时创建的 client)
8、通过 aeCreateFileEvent 方法注册文件写事件并绑定 sendReplyToClient 方法
(点击放大图像)
此时按照惯例,aeMain 主函数循环,监测到新注册的事件,调用 sendReplyToClient 方法。sendReplyToClient 方法主要包含两个操作:
1、将 outbuf 内容写入到套接字描述符并传输到客户端,主要方法如下:
- nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
2、aeDeleteFileEvent 用于删除 文件写事件
(点击放大图像)
客户端接收到服务器端的返回调用 redisBufferRead 方法,该方法主要用于从 socket 中读取数据。主要方法如下:
- nread = read(c->fd,buf,sizeof(buf));
并且将读取的数据交由 redisReaderFeed 方法,该方法主要用于将数据交给回复解析器处理,也就是 cliFormatReplyRaw,该方法将回复内容格式化。最终通过
- fwrite(out,sdslen(out),1,stdout);
方法返回给客户端并打印展示给用户。
至此整个写入流程完成。以上还有很多细节没有说到,感兴趣的朋友可以自行阅读源码。
在深入了解一个 DB 的时候,我的第一步就是去理解它执行一条命令执行的整个流程,这样就能对它整个运行流程较为熟悉,接着我们可以去深入各个细节的部分,比如 Redis 的相关数据结构、持久化以及高可用相关的东西。写这篇文章的初衷就是希望我们更加轻松的走好这第一步。这里还需要提醒的是,在我们进行 Redis 源码阅读的时候最关键的是需要灵活的使用 GDB 调试工具,它能帮我们更好的去理顺相关执行步骤,从而让我们更加容易理解其实现原理。
附录:两个相关重要知识点
1、Linux Socket 建立流程
(点击放大图像)
linux socket 建立过程如上图所示。在 Linux 编程时,无论是操作文件还是网络操作时都是通过文件描述符来进行读写的,但是他们有一点区别,这里我们不具体讨论,我们将网络操作时就称为套接字描述符。大家可以自行用 c 写一个简单的 demo,这里就不详细说明了。
这里列出几个重要的方法:
- int socket(int family,int type,int protocol);
- int connect(int sockfd,const struct sockaddr * servaddr,socklen_taddrlen);
- int bind(int sockfd,const struct sockaddr * myaddr,socklen_taddrlen);
- int listen(int sockfd,int backlog);
- int accept(int sockfd,struct sockaddr *cliaddr,socklen_t * addrlen);
Redis client/server 也是基于 linux socket 连接进行交互,并且最终调用以上方法绑定 IP,监听端口最终与客户端建立连接。
2、epoll I/O 多路复用技术
这里重点介绍一下 epoll,因为 Redis 事件管理器核心实现基本依赖于它。首先来看 epoll 是什么,它能做什么?
epoll 是在 Linux 2.6 内核中引进的,是一种强大的 I/O 多路复用技术,上面我们已经说到在进行网络操作的时候是通过文件描述符来进行读写的,那么平常我们就是一个进程操作一个文件描述符。然而 epoll 可以通过一个文件描述符管理多个文件描述符,并且不阻塞 I/O。这使得我们单进程可以操作多个文件描述符,这就是 redis 在高并发性能还如此强大的原因之一。
下面简单介绍 epoll 主要的三个方法:
Redis 的事件管理器主要是基于 epoll 机制,先采用 epoll_ctl 方法 注册事件,然后再使用 epoll_wait 方法取出已经注册的事件。
我们知道 redis 支持多种平台,那么 redis 在这方面是如何兼容其他平台的呢?Redis 会根据操作系统的类型选择对应的 IO 多路复用实现。
- #ifdef HAVE_EVPORT
- #include "ae_evport.c"
- #else
- #ifdef HAVE_EPOLL
- #include "ae_epoll.c"
- #else
- #ifdef HAVE_KQUEUE
- #include "ae_kqueue.c"
- #else
- #include "ae_select.c"
- #endif
- #endif
- #endif
- ae_evport.c sun solaris
- ae_poll.c linux
- ae_select.c unix/linux epoll是select的加强版
- ae_kqueue BSD/Apple
以上只是简单的介绍,大家需要详细了解了 epoll 机制才能更好的理解后面的东西。
来源: