今天我们来了解一下 Redis 命令执行的过程。在之前的文章中《当 Redis 发生高延迟时,到底发生了什么》我们曾简单的描述了一条命令的执行过程,本篇文章展示深入说明一下,加深读者对 Redis 的了解。
上篇
如下图所示,一条命令执行完成并且返回数据一共涉及三部分,第一步是建立连接阶段,响应了socket的建立,并且创建了client对象;第二步是处理阶段,从socket读取数据到输入缓冲区,然后解析并获得命令,执行命令并将返回值存储到输出缓冲区中;第三步是数据返回阶段,将返回值从输出缓冲区写到socket中,返回给客户端,最后关闭client。
这三个阶段之间是通过事件机制串联了,在 Redis 启动阶段首先要注册socket连接建立事件处理器:
- 当客户端发来建立socket的连接的请求时,对应的处理器方法会被执行,建立连接阶段的相关处理就会进行,然后注册socket读取事件处理器
- 当客户端发来命令时,读取事件处理器方法会被执行,对应处理阶段的相关逻辑都会被执行,然后注册socket写事件处理器
- 当写事件处理器被执行时,就是将返回值写回到socket中。
接下来,我们分别来看一下各个步骤的具体原理和代码实现。
启动时监听socket
Redis 服务器启动时,会调用 initServer 方法,首先会建立 Redis 自己的事件机制 eventLoop,然后在其上注册周期时间事件处理器,最后在所监听的 socket 上创建文件事件处理器,监听 socket 建立连接的事件,其处理函数为 acceptTcpHandler。
1 void initServer(void) { // server.c
2 ....
3 //创建aeEventLoop
4 server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
5 if (server.el == NULL) {
6 serverLog(LL_WARNING,
7 "Failed creating the event loop. Error message: '%s'",
8 strerror(errno));
9 exit(1);
10 }
11 server.db = zmalloc(sizeof(redisDb)*server.dbnum);
12 /* Open the TCP listening socket for the user commands. */
13
14 if (server.port != 0 &&
15 listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
16 exit(1);
17
18 ···
19
20 /**
21 * 注册周期时间事件,处理后台操作,比如说客户端操作、过期键等
22 */
23 if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
24 serverPanic("Can't create event loop timers.");
25 exit(1);
26 }
27 /**
28 * 为所有监听的socket创建文件事件,监听可读事件;事件处理函数为acceptTcpHandler
29 *
30 */
31 for (j = 0; j < server.ipfd_count; j++) {
32 if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
33 acceptTcpHandler,NULL) == AE_ERR)
34 {
35 serverPanic(
36 "Unrecoverable error creating server.ipfd file event.");
37 }
38 }
39 ....
40 }
我们曾详细介绍过 Redis 的事件机制,可以说,Redis 命令执行过程中都是由事件机制协调管理的,也就是 initServer 方法中生成的 aeEventLoop。当socket发生对应的事件时,aeEventLoop 对调用已经注册的对应的事件处理器。
建立连接和Client
当客户端向 Redis 建立 socket时,aeEventLoop 会调用 acceptTcpHandler 处理函数,服务器会为每个链接创建一个 Client 对象,并创建相应文件事件来监听socket的可读事件,并指定事件处理函数。acceptTcpHandler 函数会首先调用 anetTcpAccept
方法,它底层会调用 socket 的 accept 方法,也就是接受客户端来的建立连接请求,然后调用 acceptCommonHandler
方法,继续后续的逻辑处理。
1 /**
2 * 创建一个TCP的连接处理程序
3 *
4 * 为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器。
5 * 这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accept函数的包装。
6 * 当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,
7 * 当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候, 套接字就会产生AE_READABLE 事件,
8 * 引发连接应答处理器执行, 并执行相应的套接字应答操作,
9 */
10 void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
11 //#define MAX_ACCEPTS_PER_CALL 1000
12 int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
13 char cip[NET_IP_STR_LEN];
14 UNUSED(el);
15 UNUSED(mask);
16 UNUSED(privdata);
17
18 while(max--) {
19 cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
20 if (cfd == ANET_ERR) {
21 if (errno != EWOULDBLOCK)
22 //连接失败,日志记录
23 serverLog(LL_WARNING,
24 "Accepting client connection: %s", server.neterr);
25 return;
26 }
27 //连接成功,日志记录
28 serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
29 //为通信文件描述符创建对应的客户端结构体
30 acceptCommonHandler(cfd,0,cip);
31 }
32 }
acceptCommonHandler 则首先调用 createClient 创建 client,接着判断当前 client 的数量是否超出了配置的 maxclients,如果超过,则给客户端发送错误信息,并且释放 client。
1 #define MAX_ACCEPTS_PER_CALL 1000
2 // TCP连接处理程序,创建一个client的连接状态
3 static void acceptCommonHandler(int fd, int flags, char *ip) {
4 client *c;
5 // 创建一个新的client
6 if ((c = createClient(fd)) == NULL) {
7 serverLog(LL_WARNING,
8 "Error registering fd event for the new client: %s (fd=%d)",
9 strerror(errno),fd);
10 close(fd); /* May be already closed, just ignore errors */
11 return;
12 }
13 /**
14 * If maxclient directive is set and this is one client more... close the
15 * connection. Note that we create the client instead to check before
16 * for this condition, since now the socket is already set in non-blocking
17 * mode and we can send an error for free using the Kernel I/O
18 *
19 * 如果新的client超过server规定的maxclients的限制,那么想新client的fd写入错误信息,关闭该client
20 * 先创建client,在进行数量检查,是因为更好的写入错误信息
21 */
22 if (listLength(server.clients) > server.maxclients) {
23 char *err = "-ERR max number of clients reached
";
24
25 /* That's a best effort error message, don't check write errors */
26 if (write(c->fd,err,strlen(err)) == -1) {
27 /* Nothing to do, Just to avoid the warning... */
28 }
29 // 更新拒接连接的个数
30 server.stat_rejected_conn++;
31 freeClient(c);
32 return;
33 }
34
35 /**
36 * If the server is running in protected mode (the default) and there
37 * is no password set, nor a specific interface is bound, we don't accept
38 * requests from non loopback interfaces. Instead we try to explain the
39 * user what to do to fix it if needed.
40 *
41 * 如果服务器正在以保护模式运行(默认),且没有设置密码,也没有绑定指定的接口,
42 * 我们就不接受非回环接口的请求。相反,如果需要,我们会尝试解释用户如何解决问题
43 */
44 if (server.protected_mode &&
45 server.bindaddr_count == 0 &&
46 server.requirepass == NULL &&
47 !(flags & CLIENT_UNIX_SOCKET) &&
48 ip != NULL)
49 {
50 if (strcmp(ip,"127.0.0.1") && strcmp(ip,"::1")) {
51 char *err =
52 "-DENIED Redis is running in protected mode because protected "
53 "mode is enabled, no bind address was specified, no "
54 "authentication password is requested to clients. In this mode "
55 "connections are only accepted from the loopback interface. "
56 "If you want to connect from external computers to Redis you "
57 "may adopt one of the following solutions: "
58 "1) Just disable protected mode sending the command "
59 "'CONFIG SET protected-mode no' from the loopback interface "
60 "by connecting to Redis from the same host the server is "
61 "running, however MAKE SURE Redis is not publicly accessible "
62 "from internet if you do so. Use CONFIG REWRITE to make this "
63 "change permanent. "
64 "2) Alternatively you can just disable the protected mode by "
65 "editing the Redis configuration file, and setting the protected "
66 "mode option to 'no', and then restarting the server. "
67 "3) If you started the server manually just for testing, restart "
68 "it with the '--protected-mode no' option. "
69 "4) Setup a bind address or an authentication password. "
70 "NOTE: You only need to do one of the above things in order for "
71 "the server to start accepting connections from the outside.
";
72 if (write(c->fd,err,strlen(err)) == -1) {
73 /* Nothing to do, Just to avoid the warning... */
74 }
75 // 更新拒接连接的个数
76 server.stat_rejected_conn++;
77 freeClient(c);
78 return;
79 }
80 }
81
82 // 更新连接的数量
83 server.stat_numconnections++;
84 // 更新client状态的标志
85 c->flags |= flags;
86 }
createClient 方法用于创建 client,它代表着连接到 Redis 客户端,每个客户端都有各自的输入缓冲区和输出缓冲区,输入缓冲区存储客户端通过 socket 发送过来的数据,输出缓冲区则存储着 Redis 对客户端的响应数据。client一共有三种类型,不同类型的对应缓冲区的大小都不同。
- 普通客户端是除了复制和订阅的客户端之外的所有连接
- 从客户端用于主从复制,主节点会为每个从节点单独建立一条连接用于命令复制
- 订阅客户端用于发布订阅功能
createClient 方法除了创建 client 结构体并设置其属性值外,还会对 socket进行配置并注册读事件处理器,设置 socket 为 非阻塞 socket、设置 NO_DELAY 和 SO_KEEPALIVE标志位来关闭 Nagle 算法并且启动 socket 存活检查机制。设置读事件处理器,当客户端通过 socket 发送来数据后,Redis 会调用 readQueryFromClient 方法。
1 client *createClient(int fd) { 2 //分配空间 3 client *c = zmalloc(sizeof(client)); 4 5 /** 6 * passing -1 as fd it is possible to create a non connected client. 7 * This is useful since all the commands needs to be executed 8 * in the context of a client. When commands are executed in other 9 * contexts (for instance a Lua script) we need a non connected client. 10 * 11 * 如果fd为-1,表示创建的是一个无网络连接的伪客户端,用于执行lua脚本的时候。 12 * 如果fd不等于-1,表示创建一个有网络连接的客户端 13 */ 14 if (fd != -1) { 15 // 设置fd为非阻塞模式 16 anetNonBlock(NULL,fd); 17 // 禁止使用 Nagle 算法,client向内核递交的每个数据包都会立即发送给server出去,TCP_NODELAY 18 anetEnableTcpNoDelay(NULL,fd); 19 // 如果开启了tcpkeepalive,则设置 SO_KEEPALIVE 20 if (server.tcpkeepalive) 21 anetKeepAlive(NULL,fd,server.tcpkeepalive);// 设置tcp连接的keep alive选项 22 /** 23 * 使能AE_READABLE事件,readQueryFromClient是该事件的回调函数 24 * 25 * 创建一个文件事件状态el,且监听读事件,开始接受命令的输入 26 */ 27 if (aeCreateFileEvent(server.el,fd,AE_READABLE, 28 readQueryFromClient, c) == AE_ERR) 29 { 30 close(fd); 31 zfree(c); 32 return NULL; 33 } 34 } 35 36 // 默认选0号数据库 37 selectDb(c,0); 38 uint64_t client_id; 39 // 设置client的ID 40 atomicGetIncr(server.next_client_id,client_id,1); 41 c->id = client_id; 42 // client的套接字 43 c->fd = fd; 44 // client的名字 45 c->name = NULL; 46 // 回复固定(静态)缓冲区的偏移量 47 c->bufpos = 0; 48 c->qb_pos = 0; 49 // 输入缓存区 50 c->querybuf = sdsempty(); 51 c->pending_querybuf = sdsempty(); 52 // 输入缓存区的峰值 53 c->querybuf_peak = 0; 54 // 请求协议类型,内联或者多条命令,初始化为0 55 c->reqtype = 0; 56 // 参数个数 57 c->argc = 0; 58 // 参数列表 59 c->argv = NULL; 60 // 当前执行的命令和最近一次执行的命令 61 c->cmd = c->lastcmd = NULL; 62 // 查询缓冲区剩余未读取命令的数量 63 c->multibulklen = 0; 64 // 读入参数的长度 65 c->bulklen = -1; 66 // 已发的字节数 67 c->sentlen = 0; 68 // client的状态 69 c->flags = 0; 70 // 设置创建client的时间和最后一次互动的时间 71 c->ctime = c->lastinteraction = server.unixtime; 72 // 认证状态 73 c->authenticated = 0; 74 // replication复制的状态,初始为无 75 c->replstate = REPL_STATE_NONE; 76 // 设置从节点的写处理器为ack,是否在slave向master发送ack 77 c->repl_put_online_on_ack = 0; 78 // replication复制的偏移量 79 c->reploff = 0; 80 c->read_reploff = 0; 81 // 通过ack命令接收到的偏移量 82 c->repl_ack_off = 0; 83 // 通过ack命令接收到的偏移量所用的时间 84 c->repl_ack_time = 0; 85 // 从节点的端口号 86 c->slave_listening_port = 0; 87 // 从节点IP地址 88 c->slave_ip[0] = '