Redis网络模型总结
ae.c文件判断当前操作系统是否支持ae_evport,如果支持则引入ae_evport.c;如果不支持,则判断是否支持ae_poll,如果支持则引入ae_poll;以此类推,如果都不支持,则引入ae_select.c。
不同操作系统支持不同的IO多路复用实现方案,所以需要引入不同的 .c 文件:
- epoll:linux
- evport:Solaris
- kqueue:Unix、Macos
- select:基本上所有操作系统都支持
redis实现这四个 .c 文件时,都给它们暴露了统一的 API 接口。即都暴露了如:aeApiCreate(aeEventLoop *)
、aeApiResize(aeEventLoop *, int)
、aeApiFree(aeEventLoop *)
、aeApiAddEvent(aeEventLoop *, int, int)
……等接口。
流程如下:
- 创建serverSocket,并得到对应的FD(ssfd),serverSocket用来监听客户端的连接请求。
- 将ssfd注册到红黑树中,用来监听serverSocket对应的FD。
- 客户端发起连接请求时,serverSocket会产生可读事件。
- 服务端接收到可读事件后先判断是不是ssfd可读,如果是则说明这是一个客户端连接请求,从而调用accept()方法获取客户端的socket,并将客户端的FD注册到红黑树中。
- 之后服务端接收到可读事件后,有可能是ssfd产生的,也有可能是客户端fd产生的。如果是ssfd产生的读请求,说明有新的客户端连接请求,如果是客户端fd产生的读请求,说明是客户端发起的读写数据请求,redis解析请求,并返回响应结果。
Redis源码解读
1. redis服务启动入口
2. 初始化服务
- aeCreateEventLoop:创建事件循环(创建epoll实例,也就是红黑树+事件就绪列表),底层会调用
aeApiCreate
方法; - 创建serverSocket,并得到对应的FD(ssfd),通过TCP端口监听客户端连接请求;
- createSocketAcceptHandler(&server.ipfd, acceptHandler):注册FD(也就是刚刚创建的ssfd),内部会调用
aeApiAddEvent
方法将FD注册到红黑树中。这里注册的是serverSocket对应的FD,用来监听客户端连接请求;一旦ssfd上发生了可读事件,就会去调用对应的连接处理器acceptHandler来处理连接请求; - aeSetBeforeSleepProc(server.el, beforeSleep):调用epoll_wait之前的准备工作;
3. 开始监听事件循环(监听epoll实例)
-
initServer方法结束后,已经完成了epoll_wait之前的准备工作;
-
aeMain方法开始监听initServer中创建的epoll实例;
-
aeMain方法底层会去通过while循环调用aeProcessEvents方法,aeProcessEvents方法底层会调用aeApiPoll(epoll_wait),用来等待注册的FD就绪。
所以说aeMain方法本质上还是循环调用epoll_wait方法等待FD就绪。
-
调用aeApiPoll方法之前会去调用beforeSleep方法,这个方法是用来将过期的key进行删除的;
-
aeApiPoll方法返回numEvents,numEvents代表就绪的FD数量,然后在for循环中遍历就绪的FD列表,并调用对应的处理器;
比如serverSocket对应的ssfd对应的处理器为acceptHandler(连接处理器)
可以看到连接处理器中先接受客户端的连接socket并关联对应的fd;
接着调用connSetReadHandler方法注册客户端的FD,底层通过调用aeApiAddEvent方法实现;注册的是客户端FD的读事件(注意这里的读事件并不是代表客户端只能向Redis发送读请求;而是代表客户端可以发送读请求也可以发送写请求,Redis服务端获取到可读事件后,去获取客户端的读写请求,进而执行对应的读写请求)
一旦客户端FD发送了可读事件,就会去调用FD绑定的读处理器readQueryFromClient去处理客户端的读写请求。
至此流程如下:
4. 命令请求处理器
- readQueryFromClient方法:
这个函数的主要任务是从客户端读取查询数据,并将其存储到客户端的缓冲区中,然后解析这些数据并执行对应的命令。
- 客户端缓冲区:
client *c = connGetPrivateData(conn);
获取到当前连接所对应的客户端结构体(Redis为每个客户端连接都创建了客户端结构体,用来存放客户端的各类信息,包括客户端的请求信息等等),这个结构体中有一个用于存储从客户端读取数据的缓冲区querybuf
。 - 读取数据:
connRead(c->conn, c->querybuf + qblen, readlen);
从客户端连接c→conn
中读取数据,追加到querybuf
的末尾(读取的数据就是客户端发起的读写请求命令,将读写请求存放到对应客户端结构体的querybuf中)。 - 解析和处理命令:
*processInputBuffer(c);
解析读取到的数据,将其转化为 Redis 的命令参数数组argv
*。接着调用processCommand(c);
方法处理这些命令。
- processCommand方法:
这个函数负责根据客户端的命令参数,找到对应的 Redis 命令并执行。
-
查找命令:
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
通过命令名称查找到相应的命令处理函数c->cmd
(例如setCommand
、getCommand
)。 -
执行命令:
c->cmd->proc(c);
执行该命令处理函数,处理逻辑会依据具体的命令类型生成对应的结果。 -
回复结果:
*addReply(c, shared.pong);
将命令的执行结果*(例如PING
命令的PONG
响应)准备返回给客户端。
- addReply:
这个函数将命令执行的结果写入客户端结构体client的发送缓冲区(不是直接写到客户端的socket),并在必要时将缓冲区内容写出。
- 写入缓冲区:
_addReplyToBuffer(c, obj->ptr, sdslen(obj->ptr));
尝试将结果写入客户端client的写缓冲区c->buf
。 - 处理缓冲区满的情况:如果缓冲区已满,结果数据会被追加到客户端的
c->reply
链表中,这个链表没有容量限制(可以知道c→buf以及c→reply都是客户端client的输出缓冲区)。 - 等待写出:将客户端对象 c 添加到
server.clients_pending_write
队列中(这个队列是redis中已经定义好的一个队列,用来存放那些等待输出的客户端client),等待 Redis 的事件循环将这些数据发送给客户端。
注意:此时等待输出的客户端对象还是存放在
clients_pending_write
队列中,并没有真正的输出到客户端的socket中。
5. beforeSleep方法
- 创建迭代器 listiter,指向存放client对象的队列
clients_pending_write
列表的头部。这个列表存放的是有待写出响应数据的客户端; while (listNode *ln = listNext(&li))
通过迭代器li
遍历所有待写出的客户端,每个ln
是一个链表节点,包含了一个指向客户端结构体的指针 c ;connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier);
为当前客户端c
的连接设置一个写事件处理器。该函数的作用是:- 监听:内部调用
aeApiAddEvent(fd, WRITEABLE)
,告诉事件循环(epoll)需要监听客户端连接c->conn
(FD) 的写事件(即客户端 socket 变得可写)。 - 绑定处理器:将写事件处理器
sendReplyToClient
绑定到这个连接上。当写事件发生时(客户端socket变的可写时),sendReplyToClient
将被调用,以便将clients_pending_write
列表中客户端的响应数据写入对应客户端的 socket。
- 监听:内部调用
完整流程如下:
Redis多线程模型
单线程模型的性能瓶颈
- 解析客户端结构体queryBuf中的数据为Redis命令;
- 将Redis响应数据排队依次写入对应的客户端socket;
Redis 6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。