美文网首页
7. redis命令执行过程

7. redis命令执行过程

作者: 都是浮云啊 | 来源:发表于2018-11-15 22:10 被阅读0次

    这一章里看下当客户端发来一个命令,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命令执行过程

          本文链接:https://www.haomeiwen.com/subject/owbufqtx.html