这一章里看下当客户端发来一个命令,Redis是怎么处理的,也就是Redis执行来自客户端的命令过程。
Redis是单线程应用,它基于I/O多路复用技术,为了能够处理多个客户端的请求,Redis在本地为每一个链接到Redis服务器的客户端创建了一个redisClient的数据结构,这个数据结构包含了每个客户端各自的状态和执行的命令。Redis服务器使用一个链表来维护多个redisClient结构。
看下redisClient
的结构,这个reidsClient
只是redis服务的一个数据结构,当有一个客户端连接的时候就会在服务端创建这个实例,链接到链表的后面
typedef struct redisClient {
// 客户端状态标志
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 查询缓冲区
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
// 参数数量
int argc;
// 参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 请求的类型:内联命令还是多条命令
int reqtype;
// 剩余未读取的命令内容数量
int multibulklen; /* number of multi bulk arguments left to read */
// 命令内容的长度
long bulklen; /* length of bulk argument in multi bulk request */
// 回复链表
list *reply;
// 回复链表中对象的总大小
unsigned long reply_bytes; /* Tot bytes of objects in reply list */
// 已发送字节,处理 short write 用
int sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
// ...
}
部分参数说明:
- flags : 表示了目前客户端的角色,以及目前所处的状态。它比较特殊可以单独表示一个或多个状态
- querybuf:是一个SDS类型,它是一个缓冲区,用来存储没有被解析的命令
- argc&argv:当querybuf里的命令被服务器执行后,会把得到的参数个数及参数分别存储到它们俩中,argc是一个redisObject数组
- cmd: Redis使用了一个字典保存了所有的redisCommand,key是redisCommand的名字,值就是redisCommand的结构,这个结构保存了命令的实现函数,命令的标志,命令应该给定的参数个数,命令的执行次数和总消耗时长等统计信息,cmd是一个redisCommand。当解析出argv和argc后,会根据数组argc[0],到字典中查询出对应的redisCommand。假设执行了一个set指令,redis就去字典中查找
SET
这个命令对应的redisCommand。redis会执行redisCommand中对应的命令实现函数。 -
buf & bufpos & reply
: buf是一个长度为REDIS_REPLY_CHUNK_BYTES
的数组。redis执行相应的操作后,就会将需要返回的数据存储到buf中,bufpos用来记录buf中已用的字节数量,当需要恢复的数据大于REDIS_REPLY_CHUNK_BYTES
时,redis就使用reply
这个链表来存储数据。
Redis的客户端的链接和断开
redisServer
是用一个链表来维护所有的redisClient
状态,每当有一个客户端发起链接以后,就会在Redis
中生成一个对应的redisClient
数据结构,增加到clients这个链表之后,一个客户端可能被多种原因断开:
- 客户端主动退出或者进程被杀死
- timeout超时了
- Redis服务自我保护断开发数据很大的客户端
- Redis服务自我保护,客户端数量超过限制大小了就自动断开
小结:当客户端发出一个命令,Redis服务器具体执行流程如下:
- 读取套接字中的数据,写入到querybuf中
- 解析
querybuf
中的命令,记录到argc
和argv
中 - 根据
argc[0]
查找对应的reredisCommand
- 执行
redisCommand
对应的实现函数 - 执行以后将结果存入
buf&bufpos&reply
中并返回给客户端
现在再总体看下这个过程之外还需要经历那些前后的处理:
1. RedisServer的启动
Redis的main()方法如下:
int main(int argc, char **argv) {
//...
// 创建并初始化服务器数据结构
initServer();
//...
}
在这里只看这个初始化方法,它负责初始化服务器的数据结构
void initServer() {
//...
//创建eventLoop
server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
/* Create an event handler for accepting new connections in TCP and Unix
* domain sockets. */
// 为 TCP 连接关联连接应答(accept)处理器
// 用于接受并应答客户端的 connect() 调用
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
// 为本地套接字关联应答处理器
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");
//...
}
之前说过,Redis使用不同的事件处理器,处理不同的事件。初始化这个方法里初始化了事件处理器eventLoop
并且向eventLoop
中注册了两个事件处理器acceptTcpHandler
和acceptUnixHandler
分别处理远程的链接和本地链接
2. 在redisServer对redisClient实例创建
当一个客户端连接到redis服务器的时候,会触发acceptTcpHandler
事件处理器.它会创建一个链接,然后继续调用acceptCommonHandler
开始执行如下步骤:
- 调用
createClient()
方法创建redisClient
- 检查已经创建的
redisClient
是否超过 server 允许的数量的上限 - 如果超过上限就拒绝远程连接
- 否则创建
redisClient
创建成功 - 并更新连接的统计次数,更新
redisClinet
的 flags 字段
这个时候Redis在服务端创建了redisClient
数据结构,这个时候远程的客户端就在redisServer
创建了一个代理,通过这个代理实例向redis服务器发送命令
3. 处理命令
在createClient()
中
// 绑定读事件到事件 loop (开始接收命令请求)
if (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR)
向eventLoop
中注册了readQueryFromClient
用来从client中读取客户端的查询缓冲区内容然后调用processInputBuffer()
处理客户端请求。在processInputBuffer()
中,解析querybuf
中的命令,记录到argc和arhv中。然后processCommand
根据argc[0]
查找对应的redisCommand
然后执行对应的函数。在执行之前还会验证命令的正确性将结果存储buf&bufpos&reply
4. 返回数据
现在基本过程都执行了就差把命令返回客户端了
执行过程:
processCommand
->addReply
->prepareClientToWrite
向eventLoop
绑定sendReplyToClient
事件处理器,在这个事件处理器中,如果bufpos大于0,将会把buf发送给远程客户端,如果链表reply的长度大于0,就会将遍历链表reply,发送给远程的客户端,这里需要注意的是为了避免reply过大引起redis响应慢当写入总量大于设定的值的时候临时中断写入,记录操作进度,将处理时间让给其他操作,剩下的内容下次继续。
总结:
问:一个Redis命令从客户端发出到客户端收到服务器的响应经历了什么?
答:
- 远程客户端连接到redis后,redis服务器会为远程客户端创建一个
redisClient
作为代理 - redis服务器读取socket中的数据,写入querybuf中
- redis服务器解析
querybuf
中的命令,记录到argc
和argc
中 - 根据
argv[0]
查找对应的redisCommand
- 执行
redisCommand
对应的执行函数 - 执行以后将结果存储
buf&bufpos&reply
中 - 返回给客户端,这个时候会控制写入数据大小,如果过大会分段返回,保证redis的响应时间
参考:
https://www.xilidou.com/2018/03/30/redis-recommend/
《Redis设计与实现》