现在问题来了: addReplyXXX系列函数只是将回应数据写向二进制缓冲区或链表队列上, 并把客户端添加到server->clients_pending_write列表中去. 那么, 到底是什么时机, 将数据回写给客户端呢?
这个实现就非常苟了!!
按常理思维, 应该在整个 收包->解析->执行命令->写回包至缓冲区或队列这个流程中, 在整个请求体被处理结束后, 给客户端的数据连接的可写事件绑定一个事件回调, 用于写回包. 但Redis的实现, 并不是这样.
还记得, 在服务端启动流程中, 开启服务端事件循环的时候, 有这么一段代码:
int main(int argc, char ** argv) { //... aeSetBeforeSleepProc(server.el, beforeSleep); // 就是这里 aeSetAfterSleepProc(server.el, afterSleep); aeMain(server.el); aeDeleteEventLoop(server.el); return 0; }服务端的事件处理器, 有一个事前回调, 也有一个事后回调, 而处理向客户端写回包的代码, 就在事前回调中. 来看这个beforeSleep函数:
void beforeSleep(struct aeEventLoop * eventLoop) { // ... handleClientsWithPendingWrites(); // 就是在这里, 处理所有客户端的写回包 // ... }这是什么操作? 这意味着服务端的事件处理器, 每次处理一个事件之前, 都要检查一遍是否还有回应需要给客户端写. 而为什么Redis在写回包的时候, 不使用传统的注册可写事件->在可写事件回调中写回包->写成功后注销可写事件这种经典套路, Redis在handleClientsWithPendingWrites函数实现的注释中, 给出了理由: 大意就是, 不使用事件注册这种套路, 就避免了事件处理机制中会产生的系统调用开销.
我们从handleClientsWithPendingWrites函数开始, 简单的捋一下写回包是怎么实现的
int handleClientsWithPendingWrites(void) { // ... while((ln = listNext(&li))) { client * c = listNodeValue(ln); c->flags &= ~CLIENT_PENDING_WRITE; listDelNode(server.clients_pending_write, ln); // 将client->buf及client->reply中的数据写至客户端 if (writeToClient(c->fd, c, 0) == C_ERR) continue; // 如果client->reply中还有数据, 则借助事件处理器来写后续回包 // 正常情况下, 在上面的writeToClient中, 该写的数据已经全写到客户端了 // 仅有在网络异常的情况下, writeToClient处理写回包失败, 导致数据没写完时, 才会进入这个分支 // 这个逻辑是正确的: 网络有异常的情况下, 通过注册事件的形式, 待稍后再重试写回包 // 网络正常的情况下, 一次性把所有数据都写完 if (clienetHasPendingReplies(c)) { int ae_flags = AE_WRITABLE; if (server.aof_state == AOF_ON && server.aof_fsync == AOF_FSYNC_ALWAYS) { ae_flags |= AE_BARRIER; } if (aeCreateFileEvent(server.el, c->fd, ae_flags, sendReplyToClient, c) == AE_ERR) { freeClientAsync(c); } } } // ... }writeToClient函数中就写的很直白了, 就是调用write向客户端写回包. 如果client->reply上还有数据, 也一个个的写给客户端. 这里就不展示这个函数的实现了, 很简单, 没什么可说的.
sendReplyToClient几乎等同于writeToClient, 只是将后者包装成了符合事件处理回调函数签名的形式.
写回包过程中有两点需要注意:
如果writeToClient由于一些异常原因(特别是网络波动), 导致在写client->reply中的某个结点的数据失败(write报EAGAIN时), writeToClient是会返回 C_OK的, 但实际上数据未发送完毕. 这种情况下, 后续会再通过事件处理器, 来执行重试.
如果在writeToClient中, 把数据发送结束了, 在函数末尾, 是会主动删除掉对于client->fd的WRITEABLE事件的监听的.
至此, 从客户端发送请求, 到服务端接收二进制数据, 解析二进制数据, 执行命令, 再写回包的整个流程就结束了.
8. 总结Redis服务端的运行机制的核心在于事件处理器. 其实现相当朴素, 不难读懂. 事件处理器会自动选择当前操作系统平台上最优的多路IO复用接口, 这就是是为什么Redis服务端虽然是单进程单线程在跑, 但依然快成狗的原因
Redis服务端的核心代码是xxxCommand一系列命令处理函数. 这部分命令处理函数分布在t_hash.c, t_list.c, t_set.c, t_string.c, t_zset.c与server.c中, 分别是各种Redis Value Type相关的命令实现.
Redis服务端的核心数据结构实例是全局变量server. 就单机范畴的讨论来说, clients字段及其它相关字段, 持有着客户端. el字段持有着事件处理器, db字段持有着数据库
Redis协议是一个设计简单, 且阅读友好的协议
Redis服务端的启动过程其实十分简单, 在单机数据库范畴里, 主要是初始化事件处理器el与数据库db字段. 其中db字段就是初始化了一坨dict, 然后监听服务端口.