继续我们上一节的讨论。服务器启动了,客户端也发送命令了。接下来,就要到服务器“表演”的时刻了。

1 服务器处理

服务器读取到命令请求后,会进行一系列的处理。

1.1 读取命令请求

当客户端与服务器之间的套接字因客户端的写入变得可读时,服务器将调用命令请求处理器执行以下操作:

  1. 读取套接字中的命令请求,并将其保存到客户端状态的输入缓冲区。
  2. 对输入缓冲区的命令请求进行分析,提取出命令请求中包含的命令参数及参数个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里。
  3. 调用命令执行器,执行客户端指定的命令。

上面的 SET 命令保存到客户端状态的输入缓存区之后,客户端状态如图 4。

之后,分析程序将对输入缓冲区中的协议进行分析,并将得出的结果保存的客户端的 argv 和 argc 属性中,如图 5 所示:

之后,服务器将通过调用命令执行器来完成执行命令的余下步骤。

1.2 查找命令实现

命令执行器要做的第一件事就是根据 argv[0] 参数,在命令表(commandtable)中查找参数所指定的命令,并将找到的命令保存到 cmd 属性中。

命令表是一个字典,字典的键是一个个命令名称,比如 "SET"、"GET" 等。而字典的值则是一个个 redisCommand 结构,每个 redisCommand 结构记录了 Redis 命令的实现信息。源码如下:

# server.h/redisCommand
struct redisCommand {
char *name; // 命令名称。如 "SET"
redisCommandProc *proc; // 对应函数指针,指向命令的实现函数。比如 SET 对应的 setCommand 函数
int arity; // 命令参数的格个数。用来检查命令请求的格式是否合法。
// 要注意的命令的名称也是一个参数。像我们上面的 SET KEY VALUE 命令,实际上有三个参数。
char *sflags; // 字符串形式的标识值。记录了命令的属性。
int flags; // 对 sflags 标识分析得出的二进制标识,由程序自动生成。检查命令时,实际上使用的是此字段
redisGetKeysProc *getkeys_proc; // 指针函数,通过此方法来指定 key 的位置。
int firstkey; // 第一个 key 的位置
int lastkey; // 最后一个 key 的位置
int keystep; // key 之间的间距
long long microseconds, calls; // 命令的总调用时间及调用次数
};

另外,对于 sflags 属性,可使用的标识值及含义如下表:

标识 意义 带有此标识的命令
w 这是一个写入命令,可能会修改数据库 SET、RPUSH、DEL 等
r 这是一个只读命令,不会修改数据库 GET、STRLEN 等
m 此命令可能会占用大量内存,执行器需先检查内存使用情况,如果内存紧缺就禁止执行此命令 SET、APPEND、RPUSH、SADD 等
a 这是一个管理命令 SAVE、BGSAVE 等
p 这是一个发布与订阅功能的命令 PUBLISH、SUBSRIBE 等
s 这个命令不可以在 lua 脚步中使用 BPOP、BLPOP 等
R 这是一个随机命令。对于相同的数据集和相同的参数,返回结果可能不同 SPOP、SRANDMEMBER 等
S 当在 lua 脚步中使用此命令时,对返回结果进行排序,使得结果有序 SINTER、SUNION 等
l 这个命令可以在服务器载入数据的过程中使用 INFO、PUBLISH 等
t 这个命令允许在从库有过期数据时使用 SLAVEOF、PING 等
M 这个命令在监视模式下,不会被自动传播 EXEC
k 集群模式下,如果对应槽点标记位“导入”,则接受此命令 restore-asking
F 这个命令在程序执行时应该立刻执行 SETNX、GET 等

命令表结构如图 6:

对于我们上面的 SET KEY VALUE 命令,当程序以图 5 中的 argv[0] 作为输入,在命令表中进行查找时,命令表返回 "set" 键对于的 redisCommand 结构,客户端状态的 cmd 指针会指向这个 redisCommand 结构。如图 7 所示:

要注意的是,对于 Redis 而言,命令名字的大小写不影响命令表的查找结果,也就是命令名称不区分大小写。执行 SET 和 set、Set 将获得相同结果。

1.3 执行预备操作

到目前为止,服务器已经将执行命令所需要的命令实现函数(客户端 cmd 属性)、参数(客户端 argv 属性)、参数个数(客户端 argc 属性)都初始化完毕。但在真正执行命令之前,程序还会进行一些预备操作,保证命令可以正确、顺利的被执行。预备操作包括:

  1. 检查客户端的 cmd 指针是否指向 NULL,如果是的话,说明用户输入的命令名称没有对应的函数,服务器将不再执行后续操作,并向客户端返回一个错误。
  2. 根据客户端 cmd 属性指向的 redisCommand 结果的 arity 属性,检查命令请求所给定的参数个数是否正确。
  3. 检查客户端是否已经通过了身份验证。未通过身份验证的客户端只能执行 AUTH 命令。否则,将会向客户端返回一个错误。
  4. 如果服务器打开了 maxmemory 功能,在执行命令之前,会先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,将不再执行后续步骤,向客户端返回一个错误。
  5. 如果服务器上一次执行 BGSAVE 命令时出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,而将要执行的命令是一个写命令,那么服务器将拒绝执行这个鞋命令,并向客户端返回一个错误。
  6. 如果客户端正在用 SUBSCRIBEPSUBSCRIBE 命令订阅频道或模式,那么服务器只会执行客户端发来的 SUBSCRIBEPSUBSCRIBEUNSUBSCRIBEPUNSUBSCRIBE 四个命令,其它命令都会被拒绝。
  7. 如果服务器正在进行数据载入,那么客户端发送是命令必须带有 l 标识才会被服务器执行。
  8. 如果客户端正在执行事务,那么服务器只会执行 EXECDISCARDMULTIWATCH 四个命令,其他命令都会被放进事务队列中。
  9. 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。

当完成了以上预备操作之后,服务器就开始真正的执行命令了。

要注意的是,上面列出的预备操作只是服务器在单机模式下的检查操作。如果在复制或者集群模式下,预备操作还会更多。

1.4 调用命令的实现函数

在前面的操作中 ,服务器已经将要执行的命令实现、参数、参数个数保存在客户端结构中。

对于我们上面的 SET KEY VALUE 命令,图 8 包含了命令实现、参数和参数个数结构:

当服务器决定要执行命令时,只要执行以下语句即可:

// client 是指向客户端状态的指针。server.c/call()
client->cmd->proc(client);

上面的执行语句实际上就是调用 setCommand 函数(t_string.c)。

被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区中(bug 属性 和 reply 属性),之后实现函数会为客户端的套接字关联命令回复处理器,由命令回复处理器返回给客户端。

回到我们的示例,setCommand(client) 将产生一个 "+OK\r\n" 回复,这个回复被保存在客户端的 buf 属性中。如图 9 所示:

1.5 执行后续工作

实现函数执行完后,服务器还会执行一些后续工作,主要包括:

  1. 如果服务器开启了 slow-log 功能,那么慢查询日志模块将会检查是否需要将刚执行的命令添加到慢查询日志。
  2. 更新 redisCommand 结构的 milliseconds 和 calls 属性。
  3. 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区中。
  4. 如果有其它服务器正在复制当前这个服务器,那么服务器将会把刚刚执行的命令传播给所有从服务器。

以上后续操作执行完毕后,一条执行命令也就执行完成了。服务器可以继续处理后续的命令。

1.6 将命令回复发送给客户端

上面过程中,命令实现函数会将命令回复保存到客户端的输出缓冲区中,并为客户端的套接字关联命令回复处理器。当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将命令回复发送给客户端。

当命令回复发送完毕后,回复处理器会情况客户端的输出缓冲区,为处理下一个命令请求做好准备。

以图 9 所示的客户端状态为例,当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复 "+OK\r\n" 发送给客户端。

1.7 源码解读

命令处理请求,函数调用堆栈信息如图 3-7-1:

命令回复,函数调用堆栈信息如图 3-7-2:

2 客户端接收并打印回复

客户端接收到命令回复之后,会将回复转换成我们可读的格式,并打印在屏幕上(对于 redis-cli 客户端),如图 10 所示。

至此,我们走完了从发起一个命令请求,到收到回复的所有过程。对于我们最开始提的问题,服务器如何响应客户端请求,你有答案了吗?

总结

  1. 服务器通过 networking.c/readQueryFromClient() 读取和执行对应命令。
  2. 服务器通过 networking.c/writeToClient() 将命令回复发送给客户端。

跟着大彬读源码 - Redis 3 - 服务器如何响应客户端请求?(下)的更多相关文章

  1. 跟着大彬读源码 - Redis 2 - 服务器如何响应客户端请求?(上)

    上次我们通过问题"启动服务器,程序都干了什么?",跟着源码,深入了解了 Redis 服务器的启动过程. 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情.这 ...

  2. 跟着大彬读源码 - Redis 4 - 服务器的事件驱动有什么含义?(上)

    众所周知,Redis 服务器是一个事件驱动程序.那么事件驱动对于 Redis 而言有什么含义?源码中又是如何实现事件驱动的呢?今天,我们一起来认识下 Redis 服务器的事件驱动. 对于 Redis ...

  3. 跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?

    一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期. 相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入 ...

  4. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  5. 跟着大彬读源码 - Redis 7 - 对象编码之简单动态字符串

    Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis ...

  6. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  7. 跟着大彬读源码 - Redis 9 - 对象编码之 三种list

    目录 1 ziplist 2 skiplist 3 quicklist 总结 Redis 底层使用了 ziplist.skiplist 和 quicklist 三种 list 结构来实现相关对象.顾名 ...

  8. 跟着大彬读源码 - Redis 10 - 对象编码之整数集合

    [TOC] 整数集合是 Redis 集合键的底层实现之一.当一个集合只包含整数值元素,并且元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现. 1 整数集合的实现 整数集合是 Redis ...

  9. 跟着大彬读源码 - Redis 5 - 对象和数据类型(上)

    相信很多人应该都知道 Redis 有五种数据类型:字符串.列表.哈希.集合和有序集合.但这五种数据类型是什么含义?Redis 的数据又是怎样存储的?今天我们一起来认识下 Redis 这五种数据结构的含 ...

随机推荐

  1. Sql 执行查询顺序

  2. .NET中System.Diagnostics.Stopwatch、System.Timers.Timer、System.Threading.Timer 的区别

    1.System.Diagnostics.Stopwatch Stopwatch 实例可以测量一个时间间隔的运行时间,也可以测量多个时间间隔的总运行时间. 在典型的 Stopwatch 方案中,先调用 ...

  3. 数据绑定(六)使用XML数据作为Binding的Source

    原文:数据绑定(六)使用XML数据作为Binding的Source .NET Framework提供了两套处理XML数据的类库 1. 符合DOM标准的类库:包括XmlDocument.XmlEleme ...

  4. C#图片保存与读取,以及图片另存

    照片的保存与读取 /// <summary> /// 图片转二进制 /// </summary> /// <param name="imgPhoto" ...

  5. Have You Tried Delphi on Amazon Linux? (就是AWS用的Linux)

    The new Delphi Linux compiler enables customers to take new or existing Windows server applications ...

  6. Android零基础入门第40节:自定义ArrayAdapter

    原文:Android零基础入门第40节:自定义ArrayAdapter ListView用起来还是比较简单的,也是Android应用程序中最重要的一个组件,但其他ListView可以随你所愿,能够完成 ...

  7. Android之Log封装

    blog原文地址:http://yuxingxin.com/2015/10/26/AndroidLog/ Github:https://github.com/fallblank/CodeEssay

  8. C#图片灰度处理(位深度24→位深度8),用灰度数组byte[]新建一个8位灰度图像Bitmap 。

    原文:C#图片灰度处理(位深度24→位深度8) #region 灰度处理 /// <summary> /// 将源图像灰度化,并转化为8位灰度图像. /// </summary> ...

  9. Android零基础入门第71节:CardView简单实现卡片式布局

    还记得我们一共学过了多少UI控件了吗?都掌握的怎么样啊. 安卓中一些常用控件学习得差不多了,今天再来学习一个新的控件CardView,在实际开发中也有非常高的地位. 一.CardView简介 Card ...

  10. java设计模式-原型(prototype)

    有时候创建对象是需要耗费很多资源,但是每个对象之间又有大量的重复.我们可以选择在创建好一个对象后,以之作为模板克隆出其他对象,稍作修改,即可用于其他地方. 需要实现Cloneable接口,重写clon ...