Redis 请求的一次网络旅程

本文深入剖析了Redis的网络模型,从客户端连接到发送请求,再到服务器接收、解析、执行请求并响应的全过程。重点讲解了Redis的单线程IO多路复用模型,包括事件注册、连接建立、读写事件处理、命令执行及响应发送等关键步骤,展示了Redis高效处理网络请求的能力。
摘要由CSDN通过智能技术生成

Redis 请求的一次网络旅程

Redis 不必多说,可以说是目前互联网使用最广泛的内存数据库,源于其丰富的数据结构、支持数据持久化、集群、高性能的这些特性。

本文从 Redis 单线程模式,对于 Redis 客户端从登录到发送消息和收到消息的连续的行为,以 Redis 源码的角度做出解释,详细分析一个 Redis 连接的完整旅程,也作为我在 Redis 学习过程中的一个深入思考的过程。

要解释 Redis 连接的完整过程,事实上主要是对 Redis 网络模型进行分析,因此会分为这几个部分(服务端角度):注册事件、建立连接、接收请求、解析请求、 执行请求、响应请求等。

注册事件

Redis Server 首先会在 initServer() 函数中注册自身的 listenfd。

在 IO 多路复用模型中,如果要监听事件,需要将服务端的 listenfd 注册到 IO 多路复用模型中,这个行为在启动 aeMain 之前通过 createSocketAcceptHandler 已经注册到事件循环器中,这里忽略监听套接字 listenfd 的生成过程,直接进入注册监听事件的流程。

createSocketAcceptHandler 为服务端注册 listenfd 和 accept 事件,也就是接收客户端连接的处理事件,代码如下:

int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
    int j;

    for (j = 0; j < sfd->count; j++) {
        // 注册可读事件
        if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
            /* Rollback */
            for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
            return C_ERR;
        }
    }
    return C_OK;
}

createSocketAcceptHandler 通过 aeCreateFileEvent 在网络模型中注册可读事件和事件回调函数 accept_handler,该读事件回调中调用 accept 来接收客户端连接。因此所有新连接的 Redis 客户端都需要经过该函数,来建立与 Redis 服务器的连接。

在这里插入图片描述

上图表明了每次的连接到来事件,都会调用 acceptTcpHandler 进行处理,起到了事件监听的效果。

事件循环

注册好监听事件后,需要启动事件循环器来监听网络事件,Redis 服务端的 main (server.c)函数通过 aemain(ae.c) 会开启一个事件循环,从这可以看出来 Redis 的默认网络模型是单线程的IO多路复用。

在这里插入图片描述

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 处理活跃事件,事件类型默认为所有事件、before_sleep与after_sleep
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

其中真正处理事件是在 aeProcessEvents 中,默认 flag 会开启所有事件、before sleep 和 after sleep,aeProcessEvents 是 Redis 网络模块中最重要的函数,属于调度核心,源码如下:

在这里插入图片描述

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    // 即非定时事件也非网络事件,什么也不做
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

	// 如果有注册的网络事件或者有定时事件
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        struct timeval tv, *tvp;
        int64_t usUntilTimer = -1;

        // 如果有定时事件,取出最近要触发的定时事件时间
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            usUntilTimer = usUntilEarliestTimer(eventLoop);

        // 如果最近定时时间大于零则记录到 tv 中,作为事件循环的 timeout
        if (usUntilTimer >= 0) {
            tv.tv_sec = usUntilTimer / 1000000;
            tv.tv_usec = usUntilTimer % 1000000;
            tvp = &tv;
		// 如果最近定时时间小于零,也就是有待触发的定时事件
        } else {
            // 如果是 AE_DONT_WAIT 事件,设置io模型 timeout 为 0,轮询模式
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                // 一般情况设置io模型 timeout 为 NULL,也就是阻塞模式
                tvp = NULL; /* wait forever */
            }
        }
        
		// 如果是 AE_DONT_WAIT 事件,设置io模型 timeout 为 0,轮询模式
        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }
        
		// 处理 beforesleep 回调函数
        if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
            eventLoop->beforesleep(eventLoop);

        // 调用 IO 多路复用网络模型,并设置其 timeout,返回目前活跃网络事件个数
        numevents = aeApiPoll(eventLoop, tvp);

        // 处理 aftersleep 回调函数
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);

        // 循环处理活跃网络事件
        for (j = 0; j < numevents; j++) {
            int fd = eventLoop->fired[j].fd;
            // 取出活跃网络事件
            aeFileEvent *fe = &eventLoop->events[fd];
            // 取出活跃网络事件的事件类型
            int mask = eventLoop->fired[j].mask;
            int fired = 0; /* 当前 fd 已处理的事件个数 */

            // 是否设置反转标志
            int invert = fe->mask & AE_BARRIER;

            // 如果未反转,则先处理可读事件
            if (!invert && fe->mask & mask & AE_READABLE) {
                // 调用当前事件的读回调函数
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
            }

            // 处理可写事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    // 调用当前事件的写回调函数
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            // 如果设置反转,则后处理可读事件
            if (invert) {
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
                if ((fe->mask & mask & AE_READABLE) &&
                    (!fired || fe->wfileProc != fe->rfileProc))
                {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            processed++;
        }
    }
   
    // 处理定时事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
	
    // 返回已处理的网络/定时事件个数
    return processed; 
}

通过 aeProcessEvents 可以理清 Redis 网络处理的主要逻辑,大致会处理两类事件:网络事件、定时事件,对于网络事件,则是会通过 IO 多路复用器 aeApiPoll 来检测活跃事件,并依次处理这些事件的回调函数,其次对于网络事件默认会优先处理可读事件,其次处理可写事件,保证对读事件的高响应;对于定时事件,会根据最近定时触发时间来设置 IO 多路复用模型的 timeout,使得能够及时唤醒 IO 多路复用模型,防止定时事件无法及时得到响应。如果最近定时时间小于零, 会将 timeout 设为 0 以便及时处理定时事件。从该函数中看出,Redis 总是优先处理网络事件,其次处理定时事件。

事件循环器启动后,如果无时间处理,默认会阻塞在 aeApiPoll 这里,等待事件的到达或者 timeout 时间耗尽才会唤醒并处理事件。

建立连接

Redis 客户端发起连接请求,通常情况下服务器会在 aeApiPoll 阻塞中被唤醒(如果空闲),产生读事件,因此会调用 fe->rfileProc(eventLoop,fd,fe->clientData,mask) ,来进行处理该事件。

新连接事件的处理函数在 Redis 服务器 initServer 阶段被注册为 acceptTcpHandler ,因此 rfileProc 最终调用的函数是 acceptTcpHandler,通过该函数来与客户端建立连接,代码如下:

在这里插入图片描述

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    // max为每次最大接受连接的数量
    while(max--) {
        // 通过 accept 接收连接,得到客户端文件描述符 cfd
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        // 开启 cloexec 选项
        anetCloexec(cfd);
        serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
        // 创建客户端对象和连接对象
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
    }
}

这里由于服务器 listenfd 是非阻塞套接字,因此可以使用一个参数 max 来限制每次accept的连接数量。 acceptTcpHandler 中通过 anetTcpAccept 对客户端事件进行响应,得到客户端的文件描述符。其次打开了 cfd 的 cloexec 选项,该选项防止在执行 exec 系函数的进程中会继承 cfd。然后调用 acceptCommonHandler 来创建客户端对象,其中 connCreateAcceptedSocket 创建 cfd 对应的连接对象 connection:

connection *connCreateAcceptedSocket(int fd) {
    connection *conn = connCreateSocket();
    conn->fd = fd;
    conn->state = CONN_STATE_ACCEPTING;
    return conn;
}
connection *connCreateSocket() {
    connection *conn = zcalloc(sizeof(connection));
    // 设置回调函数
    conn->type = &CT_Socket;
    conn->fd = -1;

    return conn;
}

创建新的连接对象 conn,主要的工作是注册了回调函数结构体 conn->type,该结构定义了 read、write 等常规系统调用回调函数,其次设置连接的 fd、state。

创建好连接对象,便通过 acceptCommonHandler 创建客户端对象:

#define MAX_ACCEPTS_PER_CALL 1000
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
    client *c;
    char conninfo[100];
    UNUSED(ip);

    // 如果客户端连接状态为 accepting,则出错处理
    if (connGetState(conn) != CONN_STATE_ACCEPTING) {
        // 代码省略...
    }

    // 如果客户端连接超出限制,出错处理
    if (listLength(server.clients) + getClusterConnectionsCount()
        >= server.maxclients)
    {
        // 代码省略...
    }

    /* 创建客户端对象 */
    if ((c = createClient(conn)) == NULL) {
        // 代码省略...
    }

    // 设置客户端对象的状态
    c->flags |= flags;

    // 
    if (connAccept(conn, clientAcceptHandler) == C_ERR) {
       // 代码省略...
    }
}

acceptCommonHandler 中除了一些异常处理之外,最主要的是调用了 createClient 来生成客户端对象,主要代码如下:

在这里插入图片描述

client *createClient(connection *conn) {
    client *c = zmalloc(sizeof(client));

    if (conn) {
        // 设置非阻塞模式
        connNonBlock(conn);
        // 打开TCPNoDelay选项
        connEnableTcpNoDelay(conn);
        // 开启 keepalive 选项
        if (server.tcpkeepalive)
            connKeepAlive(conn,server.tcpkeepalive);
        // 绑定客户端读事件回调
        connSetReadHandler(conn, readQueryFromClient);
        // 将连接对象 private_data 设为 客户端对象 c
        connSetPrivateData(conn, c);
    }
    // 初始化 client 参数
	// 代码省略...
}

createClient 主要对客户端对象client进行创建,需要注意的是,这里考虑了 conn 为空,也就是空连接的情况,这是因为有一些客户端无需处理网络连接,比如 lua 脚本,只需要使用到客户端对象的上下文。创建客户端步骤如下:

  • 设置连接为非阻塞模式。
  • 打开 TCPNoDelay 选项,禁用了Nagle算法,允许小包的发送。
  • 开启 tcp keepalive 选项,用于开启TCP探活机制。
  • 向网络模型注册客户端读事件和其回调函数 readQueryFromClient,保证了客户端有读事件到来会调用该函数。
  • 将客户端对象设为连接对象的 private_data 成员。
  • 初始化客户端各项参数。

这里最重要的是 connSetReadHandler 注册读事件,这里最终注册的是 callHandler(connhelper.c)函数:

在这里插入图片描述

static inline int callHandler(connection *conn, ConnectionCallbackFunc handler) {
    connIncrRefs(conn);
    // 真正调用回调函数的地方(readQueryFromClient)
    if (handler) handler(conn);
    connDecrRefs(conn);
    if (conn->flags & CONN_FLAG_CLOSE_SCHEDULED) {
        if (!connHasRefs(conn)) connClose(conn);
        return 0;
    }
    return 1;
}

在上述函数中对连接对象 conn 使用了引用计数,通过 connIncrRef 和 connDecrRef,防止连接事件未处理完就被关闭。

到此为止一个新的客户端连接已经建立完毕,接下来是等待客户端的请求。

处理请求

建立完客户端连接,服务器便等待客户端的请求到来。如果此时有请求到来,同样服务器会在 aeApiPoll 阻塞中被唤醒(如果空闲),产生读事件,调用 fe->rfileProc(eventLoop,fd,fe->clientData,mask) ,其中 rfileProc 最终调用的是 readQueryFromClient 函数处理客户端请求。

在这里插入图片描述

void readQueryFromClient(connection *conn) {
    // 得到客户端对象
    client *c = connGetPrivateData(conn);
    int nread, readlen;
    size_t qblen;

    // 用于多线程 I/O
    if (postponeClientRead(c)) return;
    // 记录服务器读处理次数
    atomicIncr(server.stat_total_reads_processed, 1);
	// 每次读取的最大字节数
    readlen = PROTO_IOBUF_LEN;
    
    if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= PROTO_MBULK_BIG_ARG)
    {
        ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);

        /* Note that the 'remaining' variable may be zero in some edge case,
         * for example once we resume a blocked client after CLIENT PAUSE. */
        if (remaining > 0 && remaining < readlen) readlen = remaining;
    }
	// 计算读缓冲区已有数据大小
    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
	// 扩大缓冲区至 readlen
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    // 读取客户端数据
    nread = connRead(c->conn, c->querybuf+qblen, readlen);

    // 省略读结果处理代码
 
	// 更新接收缓冲区长度
    sdsIncrLen(c->querybuf,nread);
    
    // 省略代码

   	// 处理读缓冲区中的数据
     processInputBuffer(c);
}

readQueryFromClient 函数可以看出其主要工作是通过 connRead 将客户端请求的数据读入到客户端读缓存区 c->querybuf,connRead 是调用了 read系统调用函数来读取数据,在读取数据之前需要对客户端读缓冲区大小进行调整,防止缓冲区过小,Redis 的字符串结构 sds 有提供这类接口 sdsMakeRoomFor 用来扩充缓冲区的可用空间。获取到客户端数据后,通过 processInputBuffer 对读缓存区数据进行处理。

在这里插入图片描述

void processInputBuffer(client *c) {
    // 循环处理客户端请求,通过标识 c->qb_pos 位判断
    while(c->qb_pos < sdslen(c->querybuf)) {
        
        // 省略代码,客户端处于某些 flag 状态下无需处理请求,直接退出

        // 判断请求协议是RESP还是纯字符串inline(未编码)格式
        if (!c->reqtype) {
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }

        // 处理 inline 格式
        if (c->reqtype == PROTO_REQ_INLINE) {
            if (processInlineBuffer(c) != C_OK) break;
            // 如果开启gopher模式且非多线程模式,处理gopher协议格式
            if (server.gopher_enabled && !server.io_threads_do_reads &&
                ((c->argc == 1 && ((char*)(c->argv[0]->ptr))[0] == '/') ||
                  c->argc == 0))
            {
                processGopherRequest(c);
                resetClient(c);
                c->flags |= CLIENT_CLOSE_AFTER_REPLY;
                break;
            }
        // 处理RESP格式协议
        } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            serverPanic("Unknown request type");
        }

        // 参数为0,
        if (c->argc == 0) {
            resetClient(c);
        } else {
            // Redis多线程模式, IO 线程中不立即执行命令,直接退出
            if (c->flags & CLIENT_PENDING_READ) {
                c->flags |= CLIENT_PENDING_COMMAND;
                break;
            }

            // 终于在这里可以执行客户端命令
            if (processCommandAndResetClient(c) == C_ERR) {
                // 客户端已经不存在会返回ERR,直接退出
                return;
            }
        }
    }

    // 重置客户端请求位标识
    if (c->qb_pos) {
        sdsrange(c->querybuf,c->qb_pos,-1);
        c->qb_pos = 0;
    }
}

processInputBuffer 中需要判断客户端请求的协议格式,默认情况下是 RESP 协议,该协议能序列化整型、字符串等结构,是一种文本协议。RESP 是二进制安全的,并且不需要处理从一个进程发到另外一个进程的批量数据,因为它使用前缀长度来传输批量数据。下图说明 RESP 协议的字段含义:

img

Redis官方协议文档:Redis协议详细规范

请求的具体解析由 processMultibulkBuffer 函数来执行,这里就不展开该函数,其工作是根据 RESP 协议格式,把请求内容解析为 Redis 字符串格式,存放到客户端对象的 argc、argv 中。

解析完成后,调用 processCommandAndResetClient 来执行客户端命令,其主要调用 processCommand 来执行命令,函数定义如下:

在这里插入图片描述

int processCommand(client *c) {
    
    // 代码省略
    
    // 找到相应命令的执行函数
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    // 处理命令异常情况,省略

    // 根据命令类型设置相应字段
    int is_read_command = (c->cmd->flags & CMD_READONLY) ||
                           (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_READONLY));
    int is_write_command = (c->cmd->flags & CMD_WRITE) ||
                           (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE));
    int is_denyoom_command = (c->cmd->flags & CMD_DENYOOM) ||
                             (c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_DENYOOM));
    int is_denystale_command = !(c->cmd->flags & CMD_STALE) ||
                               (c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_STALE));
    int is_denyloading_command = !(c->cmd->flags & CMD_LOADING) ||
                                 (c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_LOADING));
    int is_may_replicate_command = (c->cmd->flags & (CMD_WRITE | CMD_MAY_REPLICATE)) ||
                                   (c->cmd->proc == execCommand && (c->mstate.cmd_flags & (CMD_WRITE | CMD_MAY_REPLICATE)));

    // 处理认证模式,代码省略

    // 处理集群模式,代码省略

    // 处理最大内存使用限制功能,如果上限会拒绝某类请求

    // 处理其他情况,代码省略

    // 处理事务
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
        c->cmd->proc != resetCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        // 执行命令
        call(c,CMD_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }

    return C_OK;
}

可以看到 processCommand 事实上主要是调用 lookupCommand 来根据请求内容查找到对应的命令执行函数,来填充客户端 cmd 字段。

其次最重要的就是调用 call 函数,该函数是真正执行客户端命令的地方,属于 Redis 执行命令的核心函数,其中调用了 c->cmd->proc(c),来调用 lookupCommand 绑定好的命令执行函数来真正执行命令。

响应请求

在调用相应的命令执行函数并执行完成后,会调用 addReply 生成响应数据,填入到客户端的写缓冲区。其函数定义如下:

在这里插入图片描述

void addReply(client *c, robj *obj) {
    if (prepareClientToWrite(c) != C_OK) return;

    if (sdsEncodedObject(obj)) {
        if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
            _addReplyProtoToList(c,obj->ptr,sdslen(obj->ptr));
    } else if (obj->encoding == OBJ_ENCODING_INT) {
        char buf[32];
        size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);
        if (_addReplyToBuffer(c,buf,len) != C_OK)
            _addReplyProtoToList(c,buf,len);
    } else {
        serverPanic("Wrong obj->encoding in addReply()");
    }
}

addReply 首先会调用 prepareClientToWrite ,该函数主要是判断客户端是否还有未发送完的数据,如果存在,将未发送完的数据附加到未发送数据尾部并注册写事件,其函数定义如下:

int prepareClientToWrite(client *c) {
    // 省略代码
    
    // 判断当前客户端发送缓冲区是否有未发送完的数据
    if (!clientHasPendingReplies(c) && !(c->flags & CLIENT_PENDING_READ))
        	// 注册写事件,将数据附加到未发送数据尾部
            clientInstallWriteHandler(c);

    return C_OK;
}

其次 addReply 会调用 _addReplyToBuffer 函数,该函数将返回消息写入客户端发送缓冲区 c->buf,如果发送缓冲区已有数据,则调用 _addReplyProtoToList 将待发送数据添加到上一次未发送数据 c->reply 尾部。

在这里插入图片描述

到此可写事件已经注册完毕,整个的客户端数据从接收到解析到处理,已经全部执行完毕,就等待响应消息发出。

Redis 如何将消息发出?是使用 beforeSleep,在文章开头介绍的事件循环函数 aeProcessEvents 中可以知道, beforeSleep 是在监听事件前执行的 hook 函数,Redis server 在初始化阶段已将 beforeSleep 绑定到 ae->before_sleep。该函数定义如下:

在这里插入图片描述

void beforeSleep(struct aeEventLoop *eventLoop) {
 
    // 省略代码
    
    handleClientsWithPendingWritesUsingThreads();

    // 省略代码
}

这里我们省略其他代码,只考虑发送消息相关代码。发送消息在 handleClientsWithPendingWritesUsingThreads 中,Redis 默认是单线程,我们不考虑多线程情况,这个函数会判断是否开启了多线程模式,如果未开启会直接调用 handleClientsWithPendingWrites。该函数定义如下:

在这里插入图片描述

int handleClientsWithPendingWrites(void) {
    listIter li;
    listNode *ln;
	// 获取未发送消息客户端个数
    int processed = listLength(server.clients_pending_write);

    listRewind(server.clients_pending_write,&li);
    // 逐个获取客户端
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        // 取消可写标志
        c->flags &= ~CLIENT_PENDING_WRITE;
        // 从未发送消息客户端list删除当前客户端
        listDelNode(server.clients_pending_write,ln);

        if (c->flags & CLIENT_PROTECTED) continue;
        if (c->flags & CLIENT_CLOSE_ASAP) continue;

        // 真正发送消息
        if (writeToClient(c,0) == C_ERR) continue;

        // 如果数据未全部发送完
        if (clientHasPendingReplies(c)) {
            int ae_barrier = 0;
            // 设置 ae_barrier 确保可写事件先与可读事件执行
            if (server.aof_state == AOF_ON &&
                server.aof_fsync == AOF_FSYNC_ALWAYS)
            {
                ae_barrier = 1;
            }
            // 注册可写事件回调 sendReplyToClient
            if (connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier) == C_ERR) {
                freeClientAsync(c);
            }
        }
    }
    return processed;
}

handleClientsWithPendingWrites 会从 clients_pending_write 链表中依次取出待发送消息的客户端,然后分别给每个客户端发送消息。从 handleClientsWithPendingWrites 函数可以看出,Redis 发送消息的逻辑是先直接调用 writeToClient 将数据发出,如果数据因为网络原因或者客户端原语未全部发送完毕,再通过 connSetWriteHandlerWithBarrier 注册可写事件。

在这里插入图片描述

writeToClient负责发送数据,writeToClient 函数定义如下:

在这里插入图片描述

int writeToClient(client *c, int handler_installed) {
    atomicIncr(server.stat_total_writes_processed, 1);

    ssize_t nwritten = 0, totwritten = 0;
    size_t objlen;
    clientReplyBlock *o;

    while(clientHasPendingReplies(c)) {
        if (c->bufpos > 0) {
            nwritten = connWrite(c->conn,c->buf+c->sentlen,c->bufpos-c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            // 如果全部发送完毕,清零相关变量
            if ((int)c->sentlen == c->bufpos) {
                c->bufpos = 0;
                c->sentlen = 0;
            }
        } else {
            // 缓冲区数据发送完毕,从 reply 中拿待发送数据
            o = listNodeValue(listFirst(c->reply));
            objlen = o->used;

            if (objlen == 0) {
                c->reply_bytes -= o->size;
                listDelNode(c->reply,listFirst(c->reply));
                continue;
            }

            // 发送数据
            nwritten = connWrite(c->conn, o->buf + c->sentlen, objlen - c->sentlen);
            // 发送失败或者未发送出任何数据则退出
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

           // 如果全部发送完毕
            if (c->sentlen == objlen) {
                c->reply_bytes -= o->size;
                listDelNode(c->reply,listFirst(c->reply));
                c->sentlen = 0;
                // 断言确保数据全部发送
                if (listLength(c->reply) == 0)
                    serverAssert(c->reply_bytes == 0);
            }
        }
        
        // 防止写操作写过多数据,slave 服务除外
        if (totwritten > NET_MAX_WRITES_PER_EVENT &&
            (server.maxmemory == 0 ||
             zmalloc_used_memory() < server.maxmemory) &&
            !(c->flags & CLIENT_SLAVE)) break;
    }
    atomicIncr(server.stat_net_output_bytes, totwritten);
    // 处理写异常
    if (nwritten == -1) {
        if (connGetState(c->conn) != CONN_STATE_CONNECTED) {
            serverLog(LL_VERBOSE,
                "Error writing to client: %s", connGetLastError(c->conn));
            freeClientAsync(c);
            return C_ERR;
        }
    }
    if (totwritten > 0) {
        if (!(c->flags & CLIENT_MASTER)) c->lastinteraction = server.unixtime;
    }
    if (!clientHasPendingReplies(c)) {
        c->sentlen = 0;
        // 多线程相关
        if (handler_installed) connSetWriteHandler(c->conn, NULL);

        // 关闭发送完毕的客户端连接
        if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
            freeClientAsync(c);
            return C_ERR;
        }
    }
    return C_OK;
}

writeToClient 会先把写缓冲区 c->buf 中的数据发送出去,然后再依次发送 c->reply 中所有的数据,如果数据未全部发送完直接退出 reply 循环。

数据未发送完毕,会将 sendReplyToClient 注册为写事件回调,其函数定义如下:

void sendReplyToClient(connection *conn) {
    client *c = connGetPrivateData(conn);
    writeToClient(c,1);
}

其实还是调用了 writeToClient 函数,直到数据发送完为止,会取消可写事件。

Redis 整个从建立连接到发送响应的流程:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值