zoukankan      html  css  js  c++  java
  • redis 命令的调用过程

    参考文献:

    1. Redis 是如何处理命令的(客户端)
    2. 我是如何通过添加一条命令学习redis源码的
    3. 从零开始写redis客户端(deerlet-redis-client)之路——第一个纠结很久的问题,restore引发的血案
    4. redis命令执行流程分析
    5. 通信协议(protocol)
    6. Redis主从复制原理
    7. Redis配置文件详解

    当用户在redis客户端键入一个命令的时候,客户端会将这个命令发送到服务端。服务端会完成一系列的操作。一个redis命令在服务端大体经历了以下的几个阶段:

    1. 读取命令请求
    2. 查找命令的实现
    3. 执行预备操作
    4. 调用命令实现函数
    5. 执行后续工作

    读取命令的请求

    从redis客户端发送过来的命令,都会在readQueryFromClient函数中被读取。当客户端和服务器的连接套接字变的可读的时候,就会触发redis的文件事件。在aeMain函数中,将调用readQueryFromClient函数。在readQueryFromClient函数中,需要完成了2件事情:

    1. 将命令的内容读取到redis客户端数据结构中的查询缓冲区。
    2. 调用processInputBuffer函数,根据协议格式,得出命令的参数等信息。
      例如命令 set key value 在query_buffer中将会以如下的格式存在:

    image

    void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
        redisClient *c = (redisClient*) privdata;
        int nread, readlen;
        size_t qblen;
        REDIS_NOTUSED(el);
        REDIS_NOTUSED(mask);
    
        // 设置服务器的当前客户端
        server.current_client = c;
    
        // 读入长度(默认为 16 MB)
        readlen = REDIS_IOBUF_LEN;
    
        ........ 
        ........
        
        // 读入内容到查询缓存
        nread = read(fd, c->querybuf+qblen, readlen);
    
        ........
        ........
        
        processInputBuffer(c);
    }
    

    命令参数的解析

    在上一节中,我们看到在readQueryFromClient函数中会将套接字中的数据读取到redisClient的queryBuf中。而对于命令的处理,实际是在processInputBuffer函数中进行的。
    在函数中主要做了以下的2个工作:

    1. 判断请求的类型,例如是内联查询还是多条查询。具体的区别可以在通信协议(protocol)里面看到。本文就不详细叙述了。
    2. 根据请求的类型,调用不同的处理函数:
      2.1 processInlineBuffer
      2.2 processMultibulkBuffer
    // 处理客户端输入的命令内容
    void processInputBuffer(redisClient *c) {
        while(sdslen(c->querybuf)) {
    
            .......
            .......
    
            /* Determine request type when unknown. */
            // 判断请求的类型
            // 两种类型的区别可以在 Redis 的通讯协议上查到:
            // http://redis.readthedocs.org/en/latest/topic/protocol.html
            // 简单来说,多条查询是一般客户端发送来的,
            // 而内联查询则是 TELNET 发送来的
            if (!c->reqtype) {
                if (c->querybuf[0] == '*') {
                    // 多条查询
                    c->reqtype = REDIS_REQ_MULTIBULK;
                } else {
                    // 内联查询
                    c->reqtype = REDIS_REQ_INLINE;
                }
            }
    
            // 将缓冲区中的内容转换成命令,以及命令参数
            if (c->reqtype == REDIS_REQ_INLINE) {
                if (processInlineBuffer(c) != REDIS_OK) break;
            } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
                if (processMultibulkBuffer(c) != REDIS_OK) break;
            } else {
                redisPanic("Unknown request type");
            }
    
            /* Multibulk processing could see a <= 0 length. */
            if (c->argc == 0) {
                resetClient(c);
            } else {
                /* Only reset the client when the command was executed. */
                // 执行命令,并重置客户端
                if (processCommand(c) == REDIS_OK)
                    resetClient(c);
            }
        }
    }
    
    

    processMultibulkBuffer 和 processInlineBuffer

    processMultibulkBuffer主要完成的工作是将 c->querybuf 中的协议内容转换成 c->argv 中的参数对象。 比如 *3 $3 SET $3 MSG $5 HELLO 将被转换为:

     argv[0] = SET
     argv[1] = MSG
     argv[2] = HELLO
    

    具体的过程就不贴代码了。同样processInlineBuffer也会完成将c->querybuf 中的协议内容转换成 c->argv 中的参数的工作。

    查找命令的实现

    到了这一步,准备工作都做完了。redis服务器已将查询缓冲中的命令转换为参数对象了。接下来将调用processCommand函数进行命令的处理。processCommand函数比较长,接下来我们分段进行解析。

    查找命令

    服务器端首先开始查找命令。主要就是使用lookupCommand函数,根据命令对应的名字,去找到对应的执行函数以及相关的属性信息。

        // 特别处理 quit 命令
        if (!strcasecmp(c->argv[0]->ptr,"quit")) {
            addReply(c,shared.ok);
            c->flags |= REDIS_CLOSE_AFTER_REPLY;
            return REDIS_ERR;
        }
    
        /* Now lookup the command and check ASAP about trivial error conditions
         * such as wrong arity, bad command name and so forth. */
        // 查找命令,并进行命令合法性检查,以及命令参数个数检查
        c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
        if (!c->cmd) {
            // 没找到指定的命令
            flagTransaction(c);
            addReplyErrorFormat(c,"unknown command '%s'",
                (char*)c->argv[0]->ptr);
            return REDIS_OK;
        } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
                   (c->argc < -c->cmd->arity)) {
            // 参数个数错误
            flagTransaction(c);
            addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
                c->cmd->name);
            return REDIS_OK;
        }
    

    那么命令的定义在哪里呢?答案在redis.c文件中,定义了一个如下的实现:

    struct redisCommand redisCommandTable[]= {
        .....
        
        {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
        
        .....
    }
    

    Redis将所有它能支持的命令以及对应的“命令处理函数”之间对应关系存放在数组redisCommandTable[]中,该数组中保存元素的类型为结构体redisCommand,此中包括命令的名字以及对应处理函数的地址,在Redis服务初始化的时候,这个结构体会在初始化函数中被转换成struct redisServer结构体中的一个dict,这个dict被赋值到commands域中。结构体详细的实现如下:

    /*
     * Redis 命令
     */
    struct redisCommand {
    
        // 命令名字
        char *name;
    
        // 实现函数
        redisCommandProc *proc;
    
        // 参数个数
        int arity;
    
        // 字符串表示的 FLAG
        char *sflags; /* Flags as string representation, one char per flag. */
    
        // 实际 FLAG
        int flags;    /* The actual flags, obtained from the 'sflags' field. */
    
        /* Use a function to determine keys arguments in a command line.
        ┆* Used for Redis Cluster redirect. */
        // 从命令中判断命令的键参数。在 Redis 集群转向时使用。
        redisGetKeysProc *getkeys_proc;
    
        /* What keys should be loaded in background when calling this command? */
        // 指定哪些参数是 key
        int firstkey; /* The first argument that's a key (0 = no keys) */
        int lastkey;  /* The last argument that's a key */
        int keystep;  /* The step between first and last key */
    
        // 统计信息
        // microseconds 记录了命令执行耗费的总毫微秒数
        // calls 是命令被执行的总次数
        long long microseconds, calls;
    }
    
    

    根据这个结构体,我们可以看到set执行的信息如下:

    1. 命令名称是set
    2. 执行函数是setCommand
    3. 参数个数是3

    执行命令前的准备工作

    在上节,我们看到了Redis是如何查找命令,以及一个命令最终的定义和实现是在哪里的。接下来我们来看下 processCommand后面部分的实现。这部分主要的工作是在执行命令之前做一点的检查工作 :

    1. 检查认证信息,如果redis服务器配置有密码,在此处会做一次验证
    2. 集群模式下的处理,此处不多做展开。
    3. 检查是否到了Redis配置文件中,限制的最大内存数。如果达到了限制,需要根据配置的内存释放策略做一定的释放操作。
    4. 检查是否主服务,并且这个服务器之前是否执行 BGSAVE 时发生了错误,如果发生了错误则不执行。
    5. 如果Redis服务器打开了min-slaves-to-write配置,则没有足够多的slave可写的时候,拒绝执行写操作。
    6. 如果当前的Redis服务器是个只读的slave的话,拒绝执行写操作。
    7. 当redis处于发布和订阅上下文的时候,只能执行订阅和退订相关的命令。
    8. 如果slave-serve-stale-data 配置为no的时候,只允许INFO 和 SLAVEOF 命令。( Redis配置文件详解)
    9. 如果服务器正在载入数据到数据库,那么只执行带有 REDIS_CMD_LOADING 标识的命令,否则将出错。
    10. 如果Lua 脚本超时,只允许执行限定的操作,比如 SHUTDOWN 和 SCRIPT KILL。

    到此Redis执行一个命令前的检查工作基本算完成了。接下来将调用call函数执行命令。

    调用命令实现函数

    在call函数里面,在真正的执行一个命令的实现函数。

    // 执行实现函数
    c->cmd->proc(c);
    

    那么这个c是指什么呢?我们来看下call函数的定义:

    void call(redisClient *c, int flags) 
    

    可见call函数传入的是redisClient这个结构体的指针。那么这个结构体在哪里创建的呢?是在"读取命令的请求"的阶段就已经创建好了。在redisClient中,定义了一个struct redisCommand *cmd 属性,在查找命令的阶段便被赋予了对应命令的执行函数。因此在此处,将会调用对应的函数完成命令的执行。

    typedef struct redisClient {
    	 // 记录被客户端执行的命令
        struct redisCommand *cmd, *lastcmd;
    }
    

    执行后续工作

    在执行完命令的实现函数之后,Redis还有做一些后续工作包括:

    1. 计算命令的执行时间
    2. 计算命令执行之后的 dirty 值
    3. 是否需要将命令记录到SLOWLOG中
    4. 命令复制到 AOF 和 slave 节点
  • 相关阅读:
    Json对象和字符串互转
    JSNOP调用。。。
    org.hibernate.LazyInitializationException: could not initialize proxy no Session
    myeclipse生成注解实体
    jquery判断浏览器和版本
    JSTL XML标签库
    ORACLE 月份不全,补全月份的sql
    js 上下左右键控制焦点
    google gson使用
    js判断undefined类型
  • 原文地址:https://www.cnblogs.com/bush2582/p/9326745.html
Copyright © 2011-2022 走看看