Redis实战(10)-一条命令在Redis是如何执行的?

2023-12-13 06:17:49

Redis Server一旦和某客户端建立连接,就会在事件驱动框架中注册可读事件,对应客户端的命令请求。

整个命令处理过程可分阶段:

  • 命令解析,processInputBufferAndReplicate
  • 命令执行,processCommand
  • 结果返回,addReply

1 命令读取:readQueryFromClient

会从客户端连接的socket中,读取最大为readlen长度的数据,readlen大小为宏定义PROTO_IOBUF_LEN,默认16KB。

接着根据读取数据的情况,进行异常处理,如:

  • 数据读取失败

  • 或客户端连接关闭等

若当前客户端是主从复制中的主节点,readQueryFromClient会把读取的数据,追加到用于主从节点命令同步的缓冲区中。

最后,调用processInputBuffer,进入命令解析阶段。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
   ...
   readlen = PROTO_IOBUF_LEN;  // 从客户端socket中读取的数据长度,默认16KB
   ...
   c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);  // 给缓冲区分配空间
   nread = read(fd, c->querybuf+qblen, readlen);  // 调用read从描述符为fd的客户端socket中读取数据
    ...
    processInputBufferAndReplicate(c);  // 进一步处理读取内容
}

2 命令解析:processInputBuffer

根据当前客户端是否有CLIENT_MASTER标记,执行如下分支:

  • Case1

    客户端无CLIENT_MASTER标记,即当前客户端不属于主从的Master。processInputBufferAndReplicate直接调processInputBuffer,对客户端输入缓冲区中的命令和参数进行解析。所以在这里,实际执行命令解析的函数是processInputBuffer

  • Case2

    客户端有CLIENT_MASTER标记。processInputBufferAndReplicate除了会调用processInputBuffer,解析客户端命令,还会调用replicationFeedSlavesFromMasterStream,将主节点接收到的命令同步给从节点

最终命令解析就在processInputBuffer

  • 首先,processInputBuffer函数会执行一个while循环,不断从客户端的输入缓冲区读数据

  • 然后,判断读取到的命令格式,是否以“*”开头:

    • 命令 *开头,processInputBuffer会调processMultibulkBuffer解析读取到的命令

    • 不是*开头,即管道命令,命令和命令间用换行符\r\n分隔的。如使用Telnet发给Redis的命令就属该类型命令。processInputBuffer会调用processInlineBuffer解析命令。

命令解析完成后,processInputBuffer就会调用processCommand,进入命令处理的第三阶段:命令执行。

执行流程图

3 命令执行:processCommand

实际执行命令前的主要逻辑:

  1. processCommand调moduleCallCommandFilters,将Redis命令替换成module想替换的命令

  2. processCommand判断当前命令是否为quit命令并做相应处理

  3. processCommand调lookupCommand,在全局变量server的commands成员变量中查找相关命令

全局变量server的commands成员变量是个哈希表,定义在redisServer结构体:

commands成员变量的初始化是在initServerConfig,调用dictCreate完成哈希表创建,再调用populateCommandTable将Redis提供的命令名称和对应的实现函数,插入哈希表。

而这其中的populateCommandTable使用redisCommand结构体数组redisCommandTable。

redisCommandTable数组在server.c定义,它的每一个元素是redisCommand结构体类型的记录,对应Redis实现的一条命令。即redisCommand结构体记录当前命令所对应的实现函数。

如下代码展示GET、SET等命令信息,实现函数getCommand,setCommand:

所以lookupCommand会根据解析的命令名称,在commands对应的哈希表中查找相应命令。

查到对应命令后,processCommand就会检查,如命令参数是否有效、发送命令的用户是否进行过验证、当前内存的使用情况等。

等processCommand对命令做完各种检查,就开始执行命令,判断当前客户端是否有CLIENT_MULTI标记:

  • 有,说明要处理Redis事务相关命令

    按事务要求,调queueMultiCommand:将命令入队保存,等待后续再一把梭

  • 无,无关事务特性

    调call实际执行命令。call通过调用命令本身,即redisCommand结构体中定义的函数指针完成。每个redisCommand结构体中都定义了其对应实现函数,在redisCommandTable数组。

分布式锁的加锁操作就是使用SET命令,就通过SET命令看一个命令实际执行过程。

SET命令对应实现函数setCommand:

  • 首先会判断命令参数,如是否带有NX、EX、XX、PX等可选项,若有,就会记录这些标记
  • 然后,调用setGenericCommand:根据setCommand记录的命令参数标记,进行相应处理。如命令参数中有NX,则setGenericCommand会调用lookupKeyWrite,查找要执行SET命令的K是否已存在
  • 若K已存在,则setGenericCommand会调用addReply,返回NULL,正符合分布式锁语义。

若SET命令可正常执行,即:

  • 命令带NX选项,但K不存在

  • 或带有XX选项,但K已存在

这样setGenericCommand就会调用setKey完成KV对的实际插入:

setKey(c->db,key,val);

然后,若命令设置了TTL,setGenericCommand还会调用setExpire函数设置过期时间。最后,setGenericCommand调用addReply函数,将结果返给客户端:

addReply(c, ok_reply ? ok_reply : shared.ok);

SET命令执行流程图

无论:

  • 在命令执行过程中,发现不符合命令的执行条件
  • 或是命令能成功执行

addReply函数都会被调用以返回结果。所以,这就进入命令处理过程的最后一个阶段:结果返回阶段。

4 结果返回:addReply

调用prepareClientToWrite,并在prepareClientToWrite中调用clientInstallWriteHandler,将待写回客户端加入到全局变量server的clients_pending_write列表。

然后,addReply会调用_addReplyToBuffer等函数,将要返回的结果添加到客户端的输出缓冲区。

至此,这就是一条命令如何从读取,经过解析、执行等步骤,最终将结果返给客户端,该过程以及涉及的主要函数:

若在前面命令处理过程中,都由I/O主线程处理,则命令执行的原子性肯定能得到保证,分布式锁的原子性也相应得到保证。

FAQ

但若这个处理过程配合I/O多路复用机制和多IO线程机制,那这俩机制是在这个过程的什么阶段发挥作用?会不会影响命令执行原子性?

文章来源:https://blog.csdn.net/qq_33589510/article/details/133138114
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。