7. redis命令执行过程

这一章里看下当客户端发来一个命令,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服务器具体执行流程如下:

  1. 读取套接字中的数据,写入到querybuf中
  2. 解析querybuf中的命令,记录到argcargv
  3. 根据argc[0]查找对应的reredisCommand
  4. 执行redisCommand对应的实现函数
  5. 执行以后将结果存入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中注册了两个事件处理器acceptTcpHandleracceptUnixHandler分别处理远程的链接和本地链接

2. 在redisServer对redisClient实例创建

当一个客户端连接到redis服务器的时候,会触发acceptTcpHandler事件处理器.它会创建一个链接,然后继续调用acceptCommonHandler开始执行如下步骤:

  1. 调用 createClient() 方法创建 redisClient
  2. 检查已经创建的 redisClient 是否超过 server 允许的数量的上限
  3. 如果超过上限就拒绝远程连接
  4. 否则创建 redisClient 创建成功
  5. 并更新连接的统计次数,更新 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命令从客户端发出到客户端收到服务器的响应经历了什么?
答:

  1. 远程客户端连接到redis后,redis服务器会为远程客户端创建一个redisClient作为代理
  2. redis服务器读取socket中的数据,写入querybuf中
  3. redis服务器解析querybuf中的命令,记录到argcargc
  4. 根据argv[0]查找对应的redisCommand
  5. 执行redisCommand对应的执行函数
  6. 执行以后将结果存储buf&bufpos&reply
  7. 返回给客户端,这个时候会控制写入数据大小,如果过大会分段返回,保证redis的响应时间

参考:
https://www.xilidou.com/2018/03/30/redis-recommend/
《Redis设计与实现》

你可能感兴趣的:(7. redis命令执行过程)