chapter14 服务器

一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。

举个例子,如果我们使用客户端执行以下命令:

redis> SET KEY VALUE
OK

那么从客户端发送 SET KEY VALUE 命令到获得回复 OK 期间,客户端和服务器共需要执行以下操作:

  1. 客户端向服务器发送命令请求 SET KEY VALUE
  2. 服务器接收并处理客户端发来的命令请求 SET KEY VALUE ,在数据库中进行设置操作,并产生命令回复 OK
  3. 服务器将命令回复 OK 发送给客户端。
  4. 客户端接收服务器返回的命令回复 OK ,并将这个回复打印给用户观看。

14.1.1 发送命令请求

Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器,如图 14-1 所示


举个例子,假设客户端执行命令:

SET KEY VALUE

那么客户端会将这个命令转换成协议:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

然后将这段协议内容发送给服务器。

14.1.2 读取命令请求

扫描二维码关注公众号,回复: 897844 查看本文章

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

  1. 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里面。
  3. 调用命令执行器,执行客户端指定的命令。
继续用上一个小节的 SET 命令为例子,图 14-2 展示了程序将命令请求保存到客户端状态的输入缓冲区之后,客户端状态的样子。


之后,分析程序将对输入缓冲区中的协议:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

进行分析,并将得出的分析结果保存到客户端状态的 argv 属性和 argc 属性里面,如图 14-3 所示。


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

14.1.3 命令执行器(1):查找命令实现

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

命令表是一个字典,字典的键是一个个命令名字,比如 "set""get""del" ,等等;而字典的值则是一个个 redisCommand 结构,每个 redisCommand 结构记录了一个 Redis 命令的实现信息,表 14-1 记录了这个结构的各个主要属性的类型和作用。

表 14-1 redisCommand 结构的主要属性

属性名 类型 作用
name char * 命令的名字,比如 "set"
proc redisCommandProc * 函数指针,指向命令的实现函数,比如 setCommandredisCommandProc 类型的定义为typedef void redisCommandProc(redisClient *c);
arity int 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数 -N ,那么表示参数的数量大于等于 N 。注意命令的名字本身也是一个参数,比如说SET msg "hello world" 命令的参数是"SET""msg""hello world" ,而不仅仅是 "msg""hello world"
sflags char * 字符串形式的标识值,这个值记录了命令的属性,比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,这个命令是否允许在 Lua 脚本中使用,等等。
flags int sflags 标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性而不是 sflags属性,因为对二进制标识的检查可以方便地通过 &^~等操作来完成。
calls long long 服务器总共执行了多少次这个命令。
milliseconds long long 服务器执行这个命令所耗费的总时长。

表 14-2 sflags 属性的标识

标识 意义 带有这个标识的命令
w 这是一个写入命令,可能会修改数据库。 SETRPUSHDEL,等等。
r 这是一个只读命令,不会修改数据库。 GETSTRLENEXISTS ,等等。
m 这个命令可能会占用大量内存,执行之前需要先检查服务器的内存使用情况,如果内存紧缺的话就禁止执行这个命令。 SETAPPENDRPUSHLPUSHSADDSINTERSTORE ,等等。
a 这是一个管理命令。 SAVEBGSAVESHUTDOWN ,等等。
p 这是一个发布与订阅功能方面的命令。 PUBLISHSUBSCRIBEPUBSUB ,等等。
s 这个命令不可以在 Lua 脚本中使用。 BRPOPBLPOPBRPOPLPUSHSPOP ,等等。
R 这是一个随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同。 SPOPSRANDMEMBERSSCANRANDOMKEY ,等等。
S 当在 Lua 脚本中使用这个命令时,对这个命令的输出结果进行一次排序,使得命令的结果有序。 SINTERSUNIONSDIFFSMEMBERSKEYS ,等等。
l 这个命令可以在服务器载入数据的过程中使用。 INFOSHUTDOWNPUBLISH ,等等。
t 这是一个允许从服务器在带有过期数据时使用的命令。 SLAVEOFPINGINFO ,等等。
M 这个命令在监视器(monitor)模式下不会自动被传播(propagate)。 EXEC

图 14-4 展示了命令表的样子,并且以 SET 命令和 GET 命令作为例子,展示了 redisCommand 结构:

  • SET 命令的名字为 "set" ,实现函数为 setCommand ;命令的参数个数为 -3 ,表示命令接受三个或以上数量的参数;命令的标识为 "wm" ,表示 SET 命令是一个写入命令,并且在执行这个命令之前,服务器应该对占用内存状况进行检查,因为这个命令可能会占用大量内存。
  • GET 命令的名字为 "get" ,实现函数为 getCommand 函数;命令的参数个数为 2 ,表示命令只接受两个参数;命令的标识为 "r" ,表示这是一个只读命令。

继续之前 SET 命令的例子,当程序以图 14-3 中的 argv[0] 作为输入,在命令表中进行查找时,命令表将返回 "set" 键所对应的 redisCommand 结构,客户端状态的 cmd 指针会指向这个 redisCommand 结构,如图 14-5 所示。


命令名字的大小写不影响命令表的查找结果

因为命令表使用的是大小写无关的查找算法,无论输入的命令名字是大写、小写或者混合大小写,只要命令的名字是正确的,就能找到相应的 redisCommand 结构。

比如说,无论用户输入的命令名字是 "SET""set""SeT" 又或者 "sEt" ,命令表返回的都是同一个 redisCommand 结构。

这也是 Redis 客户端可以发送不同大小写的命令,并且获得相同执行结果的原因:

# 以下四个命令的执行效果完全一样

redis> SET msg "hello world"
OK

redis> set msg "hello world"
OK

redis> SeT msg "hello world"
OK

redis> sEt msg "hello world"
OK

14.1.4 命令执行器(2):执行预备操作

到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的 cmd 属性)、参数(保存在客户端状态的 argv 属性)、参数个数(保存在客户端状态的 argc 属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:

  • 检查客户端状态的 cmd 指针是否指向 NULL ,如果是的话,那么说明用户输入的命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。
  • 根据客户端 cmd 属性指向的 redisCommand 结构的 arity 属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。比如说,如果 redisCommand 结构的 arity 属性的值为 -3 ,那么用户输入的命令参数个数必须大于等于 3 个才行。
  • 检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行 AUTH 命令,如果未通过身份验证的客户端试图执行除 AUTH 命令之外的其他命令,那么服务器将向客户端返回一个错误。
  • 如果服务器打开了 maxmemory 功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返回一个错误。
  • 如果服务器上一次执行 BGSAVE 命令时出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。
  • 如果客户端当前正在用 SUBSCRIBE 命令订阅频道,或者正在用 PSUBSCRIBE 命令订阅模式,那么服务器只会执行客户端发来的 SUBSCRIBEPSUBSCRIBEUNSUBSCRIBEPUNSUBSCRIBE 四个命令,其他别的命令都会被服务器拒绝。
  • 如果服务器正在进行数据载入,那么客户端发送的命令必须带有 l 标识(比如 INFOSHUTDOWNPUBLISH ,等等)才会被服务器执行,其他别的命令都会被服务器拒绝。
  • 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令,其他别的命令都会被服务器拒绝。
  • 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXECDISCARDMULTIWATCH 四个命令,其他命令都会被放进事务队列中。
  • 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。

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

注意

以上只列出了服务器在单机模式下执行命令时的检查操作,当服务器在复制或者集群模式下执行命令时,预备操作还会更多一些。

14.1.5 命令执行器(3):调用命令的实现函数

在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的 cmd 属性里面,并将命令的参数和参数个数分别保存到了客户端状态的 argv 属性和 argc 属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:

/ client 是指向客户端状态的指针

client->cmd->proc(client);

因为执行命令所需的实际参数都已经保存到客户端状态的 argv 属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。

继续以之前的 SET 命令为例子,图 14-6 展示了客户端包含了命令实现、参数和参数个数的样子。


对于这个例子来说,执行语句:

client->cmd->proc(client);

等于执行语句:

setCommand(client);

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

对于前面 SET 命令的例子来说,函数调用 setCommand(client); 将产生一个 "+OK\r\n" 回复,这个回复会被保存到客户端状态的 buf 属性里面,如图 14-7 所示。


14.1.6 命令执行器(4):执行后续工作

在执行完实现函数之后,服务器还需要执行一些后续工作:

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的 redisCommand 结构的 milliseconds 属性,并将命令的 redisCommand 结构的 calls 计数器的值增一。
  • 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面。
  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。

当以上操作都执行完了之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。

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

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

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

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

14.1.8 客户端接收并打印命令回复

当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看(假设我们使用的是 Redis 自带的 redis-cli 客户端),如图 14-8 所示。


继续以之前的 SET 命令为例子,当客户端接到服务器发来的 "+OK\r\n" 协议回复时,它会将这个回复转换成 "OK\n" ,然后打印给用户看:

redis> SET KEY VALUE
OK

以上就是 Redis 客户端和服务器执行命令请求的整个过程了。

14.2 serverCron函数

    Redis服务器中的serverCron函数默认每个100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。

14.2.1 更新服务器时间缓存

    Redis服务器中有不少功能是需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存

struct redisServer {
  // ...
  // 保存了秒级精度的系统当前UNIX时间戳
  time_t unixtime;
  // 保存了毫秒级精度的系统当前UNIX时间戳
  long long mstime;
  // ...
};
  • 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器线上时间(uptime)这类对时间精确度要求不高的功能上"使用unixtime属性和mstime属性"。
  • 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最精确的系统当前时间。

14.2.2 更新LRU时钟

    lru记录的是服务器最后一次被访问的时间,是用于服务器的计算空转时长,用属性lruclock进行存储。默认情况下,每10秒更新一次。另外,每个redis对象也存了一个lru,保存的是该对象最后一次被被访问的时间。

    当要计算redis对象的空转时间,则会用服务器的lru减去redis对象的lru,获得的结果即对象的空转时长。

    redis客户端,用命令object idletime key,可以查看该key的空转时长,返回结果是以秒为单位。由于redis10秒更新一次服务器的最后访问时间,因此不是很精确。

  lruclock时钟的当前值可以通过INFO server命令的lur_clock 域查看。

14.2.3 更新服务器每秒执行命令次数

   这个不是通过扫描全部的键,而是采用抽样的方式确定的结果。每100毫秒1次,随机抽取一些键,查看最近1秒是否有操作,来确定最近1秒的操作次数。

         接着,会将这个值,与上一次的结果,取平均值,作为本次计算的每秒执行命令数。在存入结构体中,供下次取平均值使用。

14.2.4 更新服务器内存峰值记录

         redis服务器中,用stat_peak_memory记录服务器内存峰值。每次执行serverCron函数,会查看当前内存使用量,并且与stat_peak_memory比较,如果超过这个值,就更新这个属性。

14.2.5 处理SIGTERM信号

    redis服务器,用属性shutdown_asap记录当前的结果,0是不用进行操作,1的话是要求服务器尽快关闭。

   因此,服务器关闭命令shutdown执行,并不会立即关闭服务器,而是将服务器的shutdown_asap属性置成1,当下一次serverCron读取时,就会拒绝新的请求,完成当前正在执行的命令后,开始持久化相关的操作,结束持久化后才会关闭服务器。

14.2.6 管理客户端资源

主要是会检查客户端的两个内容:

         1)客户端很长时间没有和服务器响应,服务器认为该客户端超时,则会断开和该客户端的连接。

         2)当客户端在上一次执行命令请求后,输入缓冲区超过规定的长度,程序会释放输入缓冲区,并创建一个默认大小的缓冲区,防止缓冲区过分消耗。

         3)关闭输出缓冲区超出大小限制的客户端。

14.2.7 管理数据库资源

主要是检查键是否过期,并且按照配置的策略,删除过期的键。如懒惰删除、定期删除等。

14.2.8 执行被延迟的BGREWRITEAOF

      redis用属性aof_rewrite_scheduled记录是否有延迟的bgrewriteaof命令。

当执行bgsave命令期间,如果接收到bgrewriteaof命令,不会立即执行该命令,而是会将属性aof_rewrite_scheduled置成1

每次执行serverCron函数执行时,发现属性aof_rewrite_scheduled1,会检查当前是否在执行bgsave命令或bgrewriteaof命令,如果没有在执行这两个命令,则会执行bgrewriteaof命令。

14.2.9 检查持久化操作的运行状态

    redis服务器分别用rdb_child_pidaof_child_pid属性,记录rdbaof的子进程号(即子进程pid),如果没有在执行相应的持久化,则值是-1

         1)有一个值不是-1

         每次服务器检查这两个属性,发现有一个不是-1,则会检查子进程是否有信号发来服务器进程。

         如果有信号,表示rdb完成或aof重写完毕,服务器会进行后续的操作,比如用新的rdbaof替换旧的相应文件。

         如果没信号,表示持久化还没完成,程序不做动作。

         2)两个值都是-1

         两个值都不是-1,会进行三个检查:

         如果bgrewriteaof命令有存在延迟(即上述aof_rewrite_scheduled值是1),因为两个属性都是 -1,表示当前没有在持久化,则redis服务器会开始aof的重写。

         检查服务器是否满足bgsave条件,如果满足,因为两个属性都是 -1,则会开始执行bgsave

         检查服务器是否满足bgrewriteaof条件,如果满足,因为两个属性都是 -1,则会开始执行bgrewriteaof

         流程如下:


14.2.10 将AOF缓冲区中的内容写入AOF文件

    如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区的内容写入到AOF文件里面。

14.2.11 关闭异步客户端

    在这一步,服务器会关闭那些输出缓冲区大小超出限制的客户端。

14.2.12 增加cronloops计数器的值

redis用属性cronloops保存serverCron函数执行的次数。当执行一次serverCron,则会将属性值加1

这个值目前的作用,是在主从复制情况下,会有一个条件是,每执行nserverCron,则执行一次指定代码。

14.3 初始化服务器

    从启动 Redis 服务器, 到服务器可以接受外来客户端的网络连接这段时间, Redis 需要执行一系列初始化操作

14.3.1 初始化服务器状态结构

    redis中一个最重要的数据结构是redis_server,会创建一个这个结构的全局变量server,初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。初始化server变量的工作由redis.c/initServerConfig函数完成

void initServerConfig(void){
    // 设置服务器的运行id 
    getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE);
    // 为运行id加上结尾字符
    server.runid[REDIS_RUN_ID_SIZE] = '\0';
    // 设置默认配置文件路径
    server.configfile = NULL;
    // 设置默认服务器频率
    server.hz = REDIS_DEFAULT_HZ;
    // 设置服务器的运行架构
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
    // 设置默认服务器端口号
    server.port = REDIS_SERVERPORT;
  ...
}
以下是initServerConfig函数完成的主要工作:
  • 设置服务器的运行ID。
  • 设置服务器的默认运行频率。
  • 设置服务器的默认配置文件路径。
  • 设置服务器的运行架构。
  • 设置服务器的默认端口号。
  • 设置服务器的默认RDB持久化条件和AOF持久化条件。
  • 初始化服务器的LRU时钟。
  • 创建命令表。

14.3.2 载入配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。举个例子,如果我们在终端中输入:

$ redis-server --port 10086

那么我们就通过给定配置参数的方式,修改了服务器的运行端口号。另外,如果我们在终端中输入:

$ redis-server redis.conf

那么我们就通过指定配置文件的方式修改了服务器的数据库数量,以及RDB持久化模块的压缩功能。

服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。

14.3.3 初始化服务器数据结构

在之前执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,不过除了命令表之外,服务器状态还包含其他数据结构,比如:

  • server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例;
  • server.db数组,数组中包含了服务器的所有数据库;
  • 用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表;
  • 用于执行Lua脚本的Lua环境server.lua;
  • 用于保存慢查询日志的server.slowlog属性。

此时服务器将调用initServer函数为上面提到的这些数据结构进行分配内存,并在需要的时候为其关联初始化值。

除了上面这些外,initServer还进行了一些非常重要的设置操作,其中包括:

  • 为服务器设置进程信号处理器;
  • 创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含"OK"回复的字符串对象,包含"ERR"回复的字符串对象,包含整数1到10000的字符串对象等等,服务器通过重用这些共享对象来避免反复创建相同的对象;
  • 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接;
  • 为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数;
  • 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备;
  • 初始化服务器的后台I/O模块(bio),为将来的I/O操作做好准备。

14.3.4 还原数据库状态

紧接着,如果在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。

根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:

  • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态;
  • 相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态。

当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长:

[5244] 21 Nov 22:43:49.084 * DB loaded from disk: 0.068 seconds

14.3.5 执行事件循环

    在初始化最后一步,服务器会打印出一下日志

    并开始执行服务器的事件循环(loop)。

    至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的请求了。

14.4 重点回顾

  • 一个命令请求从发送到完成主要包括以下步骤:1. 客户端将命令请求发送给服务器;2. 服务器读取命令请求,并分析出命令参数;3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4. 服务器将命令回复返回给客户端。
  • serverCron 函数默认每隔 100 毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的 SIGTERM 信号,管理客户端资源和数据库状态,检查并执行持久化操作,等等。
  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:1. 初始化服务器状态;2. 载入服务器配置;3. 初始化服务器数据结构;4. 还原数据库状态;5. 执行事件循环。


























猜你喜欢

转载自blog.csdn.net/yongchaocsdn/article/details/79726915