本篇主要从代码结构的角度介绍了Redis命令请求的处理流程。
服务器处理客户端命令请求的整个流程:服务器启动监听、接收命令请求并解析、执行命令请求和返回命令回复
上一小篇分分析了服务器的启动过程,这篇主要介绍Redis的请求处理过程,此过程主要分为3个阶段:解析命令请求、调用命令和返回结果给客户端。其中,解析命令请求包含:连接建立、读取请求;然后,通过processCommand函数完成命令调用;最后将执行结果封装返回给客户端用户。
1. client与server建立连接
这部分在服务启动的时候已经建立起来了。
2. readQueryFromClient 读取请求命令
解析客户端命令请求的入口函数为readQueryFromClient()
,也就是说所有的请求都会执行到readQueryFromClient()
方法中,readQueryFromClient()
方法会从socket
中读取数据放到输入缓冲区querybuf
中,接着会调用processInputBuffer()
方法按照RESP协议来解析参数。
Redis采用自定义协议(RESP协议)格式实现不同命令请求的区分。
【注:内联命令:使用空格来分隔各个参数,服务器在接收到数据之后,会将空格作为参数分隔符解析命令请求】
【如果是telnet发送的裸协议数据是没有 “ * ” 打头的表示参数个数的辅助信息,用processInlineBuffer()函数解析输入;其他则通过processMultibulkBuffer()函数解析】
3. processCommand 处理命令请求
解析完参数之后会调用processCommand()
方法执行具体的命令,而processCommand中,在处理命令请求之前还有很多校验逻辑。
例如: 是否是quit命令;命令是否存在;参数数目是否合法;客户端是否认证通过; 内存限制校验、集群相关校验、持久化相关校验、主从复制相关校验、发布订阅相关校验及事务操作等
在processCommand()
中根据命令名称找到对应的命令并调用命令的call()
完成具体的操作,。
void call(client *c, int flags) {
// ...
start = ustime();
c->cmd->proc(c); // 在proc中调用各个命令的入口函数
duration = ustime()-start;
// 执行命令完成后,如果有必要,还需要更新统计信息,记录慢查询日志,AOF持久化该命令请求,传播命令请求给所有的从服务器等
// 更新统计信息:当前命令执行时间与调用次数
c->lastcmd->microseconds = duration;
c->lastcmd->calls ;
//记录慢查询日志
slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);
// ...
}
4. addReply 返回执行结果
命令在执行完成之后都会调用addReply()
方法返回执行结果。
Redis服务器返回结果类型不同,协议格式不同,而客户端可以根据返回结果的第一个字符判断返回类型。
代码语言:javascript复制// For Simple Strings the first byte of the reply is " "
// For Errors the first byte of the reply is "-"
// For Integers the first byte of the reply is ":"
// For Bulk Strings the first byte of the reply is "$"
// For Arrays the first byte of the reply is "*"
1)状态回复,第一个字符是“ ”;
2)错误回复,第一个字符是“-”;
3)整数回复,第一个字符是“:”;
4)批量回复,第一个字符是“$”;
5)多条批量回复,第一个字符是“*”;
但是,这里需要注意的是addReply()
方法只是把返回的数据写入到输出缓冲区client->buf
或者输出链表client->reply
中(通常缓存数据时都会先尝试缓存到buf输出缓冲区,如果失败会再次尝试缓存到reply输出链表),并不执行实际的网络发送操作。
什么时候将这些数据发送给客户端呢?
5. 发送数据给客户端
Redis在每次进入事件循环之前,都会先调用beforeSleep()
方法,实际的网络发送数据操作时在beforeSleep()
方法中完成的。
beforeSleep()
会在handleClientsWithPendingWrites()
/handleClientsWithPendingWritesUsingThread()
中遍历clients_pending_write链表中每一个客户端节点,并调用writeToClient()
方法把输出缓冲区client->buf
和client->reply
中的数据通过socket发送给客户端。
需要注意的是,当返回结果数据量非常大时,函数writeToClient执行之后,客户端输出缓冲区或者输出链表中可能还有部分数据未发送给客户端。这时候怎么办呢?很简单,只需要添加文件事件,监听当前客户端socket文件描述符的可写事件即可。
代码语言:javascript复制if (aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c) == AE_ERR){
}
当客户端可写时,函数sendReplyToClient会发送剩余部分的数据给客户端。
至此,命令请求才算是真正处理完成了。