简介
redis版本:6.0+
一、关键点
1.事件驱动架构图,从下往上分别是aeFileEvent, connection, client。
2.消息处理流程,主要有:a、主线程取得触发了读写操作的fd。b、多IO线程同步读取fd中的数据并解析数据。c、主线程处理各事件解析出来的协议请求。d、将前一步写入缓冲区的数据多IO线程发送出去。
3.多线程IO读写数据的实现。
4.其它,包括读写缓冲区的复用。
二、事件驱动架构
2.1 类图
从底向上分别是aeFileEvent,connection,clinet。
2.2 从读事件的处理过程看调用关系。
从堆栈中可以看出,调用的过程是fd->aeFileEvent->connection->client。
2.3 aeFileEvent的定义
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc; // 读事件处理函数 由上面堆栈可知绑定了connSocketEventHandler函数
aeFileProc *wfileProc; // 写事件处理函数 通常为null
void *clientData; // 指向connection的指针
} aeFileEvent
通过epoll_wait可以获取触发了读写事件的fd,如何通过fd找到对应的aeFileEvent呢?
// 首先所有的aeFileEvent都存储在aeEventLoop中 删减了部分字段
int maxfd; /* 当前最大的文件描述符id */
int setsize; /* events数组的大小 */
aeFileEvent *events; /* 注册的所有事件 */
aeFiredEvent *fired; /* 触发的IO事件*/
} aeEventLoop;
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd]; // 把fd当作events数组的下标
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc; // 绑定读处理函数
if (mask & AE_WRITABLE) fe->wfileProc = proc; // 绑定写处理函数
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
由上面可知,fd索引对应事件的方式是把fd当作aeEventLoop->events数组的下标来进行索引。之所以可以这么做是因为fd由操作系统分配,在进程中都是从0开始递增。
2.4 connection的定义
connection主要是绑定event的处理函数,以及对应的client和fd。
struct connection {
ConnectionType *type; // 绑定的其它的处理函数
ConnectionState state; // 连接状态
void *private_data; // 指向client的指针
ConnectionCallbackFunc conn_handler; // 连接处理函数
ConnectionCallbackFunc write_handler; // 写处理函数
ConnectionCallbackFunc read_handler; // 读处理函数
int fd; // 绑定的文件描述符
};
2.5.client的定义
client主要是客户端连接需要的缓存数据,包含读写缓冲区,clientid等。节选部分字段,定义如下:
typedef struct client {
uint64_t id; /* client_id. */
connection *conn; // 绑定的connection
sds querybuf; /* 读缓冲区. */
size_t qb_pos; // 读当前读缓冲已读取数据的长度
int argc; /* 协议解析后当前参数的数量. */
robj **argv; // 协议解析后保存参数的数组
char buf[PROTO_REPLY_CHUNK_BYTES] // 固定长度的写缓冲区
int bufpos; // 当前缓冲区已写数据的长度
list *reply; // 当固定缓冲区放不下所有的写数据时 将数据写入该list中
} client;
这里提一下client保存和索引的方式,client通过id存储在radix tree中。
三、消息处理的流程--多线程
3.1.1 总的处理流程
第一个被框起来的部分是多线程读取数据并解析数据.
第二个被框起来的部分是多线程将数据写回给Client.
接下来以命令:set test_key test_value来断点绘出各处理过程的堆栈。
多线程IO需要重点关注beforeSleep这个函数,该函数会先多线程读取数据并解析,然后主线程处理所有命令,最后将所有数据写回客户端。
3.1.2 开启redis的多线程IO
redis开启多线程IO
3.2 触发读事件的client加入待处理队列
堆栈图如下,在postponeClientRead中将client加入server.clients_pending_read中。
int postponeClientRead(client *c) {
if (io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
{
c->flags |= CLIENT_PENDING_READ; // 一个client只会被加入list一次
listAddNodeHead(server.clients_pending_read,c); // 将client加入待处理读请求队列
return 1;
} else {
return 0;
}
}
3.3 读取数据并解析
在beforeSleep中,取出待处理的事件,先读数据,再尝试解析协议。
在这里重点关注2个函数,readQueryFromClient和processInputBuffer。
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
// ......
nread = connRead(c->conn, c->querybuf+qblen, readlen); // 读取数据
// ......
processInputBuffer(c); // 解析读取的数据
}
void processInputBuffer(client *c) {
// ......
if (c->reqtype == PROTO_REQ_INLINE) { // 如果是内联请求
if (processInlineBuffer(c) != C_OK) break;
/* If the Gopher mode and we got zero or one argument, process
* the request in Gopher mode. */
if (server.gopher_enabled &&
((c->argc == 1 && ((char*)(c->argv[0]->ptr))[0] == '/') ||
c->argc == 0))
{
processGopherRequest(c);
resetClient(c);
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
break;
}
} else if (c->reqtype == PROTO_REQ_MULTIBULK) { // 如果是数组请求
if (processMultibulkBuffer(c) != C_OK) break;
} else {
serverPanic("Unknown request type");
}
}
这里留1个问题,当读取的数据不足以组成一个完整命令时,如何处理?
3.4 处理命令
命令处理的流程主要是将从client中获取命令名称并根据名称找到处理函数。
3.5 数据发送
四、多线程IO处理的实现
4.1 IO线程的实现
void *IOThreadMain(void *myid) {
while(1) {
/* Wait for start */
for (int j = 0; j < 1000000; j++) {
if (io_threads_pending[id] != 0) break; // 多线程模式开启但又还没有数据待处理 陷入循环 当有事件待处理时 退出去执行事件
}
/* Give the main thread a chance to stop this thread. */
if (io_threads_pending[id] == 0) { // 当没有数据可处理时走到这里
pthread_mutex_lock(&io_threads_mutex[id]);// 当多线程模式关闭时 通过锁停止线程
pthread_mutex_unlock(&io_threads_mutex[id]); // 当多线程模式开启时 获得锁 并立即释放
continue;
}
listIter li;
listNode *ln;
listRewind(io_threads_list[id],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) { // 通过全局的io_threads_op来区分是读操作还是写操作
writeToClient(c,0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
} else {
serverPanic("io_threads_op value is unknown");
}
}
// 所有事件读处理完成 清空待处理事件 会走到锁逻辑中
listEmpty(io_threads_list[id]);
io_threads_pending[id] = 0;
}
}
// 开启多线程模式时 释放锁 以便让IO线程继续执行
void startThreadedIO(void) {
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_unlock(&io_threads_mutex[j]);
io_threads_active = 1;
}
// 关闭多线程模式时 获取锁 以便让IO线程停在锁逻辑上
void stopThreadedIO(void) {
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_lock(&io_threads_mutex[j]);
io_threads_active = 0;
}
通过上面的源码不难发现以下几点 :
- 主线程和IO线程的同步通过io_threads_mutext[id]来实现, 每个线程都有一把锁, 当关闭多线程模式时, IO线程停留在获取锁上面.
- 主线程和IO线程的事件数据同步通过io_threads_list[id]和io_threads_pending[i]来实现, 前者存放事件相关的数据, 后者存放待处理IO事件的数量.
- IO线程既处理读事件, 也处理写事件, 通过全局变量io_threads_op来区分当前是读操作还是写操作.
- io_threads_active的值代表多线程模式是否开启
PS: 在上面的开启多线程IO文章中提到了, 只有待处理的写事件超过IO线程数的2倍时才会开启多线程IO. 所以大部分情况下, 可以认为IO线程卡在获取锁逻辑上.
4.2 并发处理请求
int handleClientsWithPendingReadsUsingThreads(void) {
if (!io_threads_active || !server.io_threads_do_reads) return 0; // 多线程模式开启且开启多线程读
int processed = listLength(server.clients_pending_read);
if (processed == 0) return 0;
/* 通过轮询的方式将所有事件分发给包括主线程在内的所有的IO线程 */
listIter li;
listNode *ln;
listRewind(server.clients_pending_read,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
// 设置每个IO线程需要处理事件的数量(没有主线程
io_threads_op = IO_THREADS_OP_READ; // 设置全局变量 标识当前在处理读事件
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
io_threads_pending[j] = count;
}
/* 主线程同样需要处理读事件 */
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
readQueryFromClient(c->conn);
}
listEmpty(io_threads_list[0]);
/* 等待所有IO事件处理完成 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += io_threads_pending[j];
if (pending == 0) break;
}
/* Run the list of clients again to process the new buffers. */
while(listLength(server.clients_pending_read)) {
ln = listFirst(server.clients_pending_read);
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_READ;
listDelNode(server.clients_pending_read,ln);
if (c->flags & CLIENT_PENDING_COMMAND) {
c->flags &= ~CLIENT_PENDING_COMMAND;
if (processCommandAndResetClient(c) == C_ERR) { // 在这里 主线程会处理所有的命令
/* If the client is no longer valid, we avoid
* processing the client later. So we just go
* to the next. */
continue;
}
}
processInputBuffer(c); // 这里是因为该写事件包含了多个待处理命令 但一次只能解析一个 所以还要再尝试解析缓冲区 解析下一个命令 待确认???
}
return processed;
}
通过上面的源码可以发现:
- 主线程会把待处理事件push到io_threads_list中, 然后设置待处理事件的数量, 此时IO线程就会走到循环里处理读事件.
- io_threads_op会在让IO线程处理IO线程前设置为IO_THREADS_OP_READ.
- 主线程同样会处理读事件.
4.3 并发将数据写回
int handleClientsWithPendingWritesUsingThreads(void) {
int processed = listLength(server.clients_pending_write);
if (processed == 0) return 0; /* Return ASAP if there are no clients. */
// 尝试关闭IO多线程处理模式
if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
/* 尝试开启IO多线程模式 */
if (!io_threads_active) startThreadedIO();
// 将IO写事件分发到各IO线程
listIter li;
listNode *ln;
listRewind(server.clients_pending_write,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
// 设置全局的操作模式 并且设置待处理的事件数量 让IO线程开始运行
io_threads_op = IO_THREADS_OP_WRITE;
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
io_threads_pending[j] = count;
}
/* 主线程也要处理写事件 */
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
writeToClient(c,0);
}
listEmpty(io_threads_list[0]);
/* 通过io_threads_pending[id] 判断IO线程是否完成写操作 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += io_threads_pending[j];
if (pending == 0) break;
}
// 为什么这里还要再尝试写数据 前面数据没写完???
listRewind(server.clients_pending_write,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (clientHasPendingReplies(c) &&
connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
{
freeClientAsync(c);
}
}
listEmpty(server.clients_pending_write);
return processed;
}
写流程的处理和读流程的处理相似, 这里不再赘述.
五 单线程消息处理流程
5.1 消息处理的堆栈图
流程比较简单,从rfileProc一路回调到readQueryFromClient最后找到对应命令处理函数。
总结
- 通过锁来开启和关闭来控制IO线程的运行. 锁的开启和关闭和多线程模式同步.
- 多线程IO就是在主线程处理IO事件前, 将任务分配给各个IO线程, 所有线程处理都完成后, 再由主线程统一处理所有命令.
- 命令的处理依然是单线程的, 因此所有存储数据结构的访问都不需要加锁.