redis 命令的调用过程

Posted bush2582

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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中将会以如下的格式存在:

技术分享图片

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 节点






























以上是关于redis 命令的调用过程的主要内容,如果未能解决你的问题,请参考以下文章

redis启动过程源码解析

Redis 单线程架构

redis的数据类型

Redis单线程架构以及工作方式

redis 命令执行过程

redis主从同步