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\r\n$3\r\nSET\r\n$3\r\nMSG\r\n$5\r\nHELLO\r\n将被转换为:

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

原文地址:https://www.cnblogs.com/bush2582/p/9326745.html

时间: 2024-11-10 05:29:45

redis 命令的调用过程的相关文章

深入Redis命令的执行过程

深入Redis命令的执行过程 Redis 服务器: Redis 服务器实现与多个客户端的连接,并处理这些客户端发送过来的请求,同时保存客户端执行命令所产生的数据到数据库中.Redis 服务器依靠资源管理器来维持自身的运转,其主要作用是管理 Redis 服务. 服务器处理命令的过程 我们向客户端发送了一条命令:SET city〝beijing〝 第一步 用户将命令 SET city〝beijing〝输入客户端,客户端接收到此命令. 第二步 客户端会先将接收到的命令转化为服务器可以识别的协议格式,然

Redis 命令执行过程(下)

在上一篇文章中<Redis 命令执行过程(上)>中,我们首先了解 Redis 命令执行的整体流程,然后细致分析了从 Redis 启动到建立 socket 连接,再到读取 socket 数据到输入缓冲区,解析命令,执行命令等过程的原理和实现细节.接下来,我们来具体看一下 set 和 get 命令的实现细节和如何将命令结果通过输出缓冲区和 socket 发送给 Redis 客户端. set 和 get 命令具体实现 前文讲到 processCommand 方法会从输入缓冲区中解析出对应的 redi

Redis命令参考之复制(Replication)

Redis 支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(master server)的精确复制品. 以下是关于 Redis 复制功能的几个重要方面: Redis 使用异步复制. 从 Redis 2.8 开始, 从服务器会以每秒一次的频率向主服务器报告复制流(replication stream)的处理进度. 一个主服务器可以有多个从服务器. 不仅主服务器可以有从服务器, 从服务器也可以有自己的从

REdis命令处理流程处理分析

分析版本:REdis-5.0.4. REdis命令处理流程可分解成三个独立的流程(不包括复制和持久化): 1) 接受连接请求流程: 2) 接收请求数据和处理请求流程,在这个过程并不会发送处理结果给Client,而只是将结果数据写入响应缓冲,将由响应请求流程来发送: 3) 响应请求流程. 上述三个流程均是异步化的,并且没有直接的联系.它们的共同点均是通过REdis的简单事件驱动(AE,A simple event-driven)触发,对于Linux实际是epoll的包装,对于macOS为evpor

分布式数据库中间件–(3) Cobar对简单select命令的处理过程

友情提示:非原文链接可能会影响您的阅读体验,欢迎查看原文.(http://blog.geekcome.com) 原文地址:http://blog.geekcome.com/archives/284 在上一篇中介绍了Cobar和client初次建立连接的过程,Cobar监听端口,client发起连接请求,Cobar发送握手数据包,client发送认证数据包最后依据认证的结果Cobar向client发送认证结果. 在认证成功后Cobar会将该连接的回调处理函数由FrontendAuthenticat

redis实战笔记(3)-第3章 Redis命令

第3章 Redis命令 本章主要内容 字符串命令. 列表命令和集合命令 散列命令和有序集合命令 发布命令与订阅命令 其他命令 在每个不同的数据类型的章节里, 展示的都是该数据类型所独有的. 最具代表性的命令. 首先让我们来看看, 除了GET和SET之外, Redis的字符串还支持哪些命令. 3.1 字符串 在Redis里面, 字符串可以存储以下3种类型的值. 字节串( byte string) . 整数. 浮点数. 除了自 增操作和自 减操作之外, Redis还拥有对字节串的其中一部分内容进行读

redis命令详解与使用场景举例——String

APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾. 如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样. 可用版本: 2.0.0+ 时间复杂度: 平摊O(1) 返回值: 追加 value 之后, key 中字符串的长度. 对不存在的 key 执行 APPEND redis> EXISTS myphone # 确保 myphone 不

redis两种调用方式实例

在下面的代码示例中,将给出两种最为常用的Redis命令操作方式,既普通调用方式和基于管线的调用方式.    注:在阅读代码时请留意注释. 1 #include <stdio.h>  2#include <stdlib.h>  3#include <stddef.h>  4#include <stdarg.h>  5#include <string.h>  6#include <assert.h>  7#include <hire

VB6 实现命令行调用时附着到原控制台

Public Declare Function AttachConsole Lib "kernel32.dll" (ByVal ProcessID As Integer) As Boolean 一个参数,就是进程ID.要实现附着,必须得提供其进程ID,那么就需要经过非常复杂的过程.那么,现在请大家再次鄙视下微软,因为这个 API 函数还留有另外一个常量:ATTACH_PARENT_PROCESS(十进制值:-1),能够实现直接附着到父进程(即调用程序的进程上). 那么,现在对上例教程的