服务器
2023-4-5
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
 

Redis 命令请求的执行过程

一个命令请求从发送到获得回复的过程中, 客户端和服务器需要完成一系列操作。如果使用客户端执行命令:SET KEY VALUE
从客户端发送 SET KEY VALUE 命令到获得回复 OK 期间, 客户端和服务器共需要执行以下操作:
  1. 客户端向服务器发送命令请求 SET KEY VALUE 
  1. 服务器接收并处理客户端发来的命令请求 SET KEY VALUE , 在数据库中进行设置操作, 并产生命令回复 OK 
  1. 服务器将命令回复 OK 发送给客户端
  1. 客户端接收服务器返回的命令回复 OK , 并将这个回复打印给用户观看

发送命令请求

Redis服务器的命令请求来自 Redis客户端, 当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器:
notion image
客户端会将SET KEY VALUE这个命令转换成协议:
然后将这段协议内容发送给服务器
 

读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
  1. 读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面
  1. 对输入缓冲区中的命令请求进行分析, 提取出命令请求中包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里面。
  1. 调用命令执行器, 执行客户端指定的命令。
程序将命令请求保存到客户端状态的输入缓冲区之后, 客户端状态的样子:
notion image
分析程序将对输入缓冲区中的协议进行分析,并将得出的分析结果保存到客户端状态的 argv 属性和 argc 属性里面:
notion image
之后, 服务器将通过调用命令执行器来完成执行命令所需的余下步骤
 

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

命令执行器要做的第一件事就是根据客户端状态的argv[0]参数, 在命令表(command table)中查找参数所指定的命令, 并将找到的命令保存到客户端状态的cmd属性里面。
命令表是一个字典, 字典的键是一个个命令名字,比如"set""get""del",等等; 而字典的值则是一个个redisCommand结构, 每个redisCommand结构记录了一个Redis命令的实现信息。
redisCommand 结构的主要属性:
属性名
类型
作用
name
char *
命令的名字,比如 "set" 
proc
redisCommandProc *
函数指针,指向命令的实现函数,比如setCommand。 redisCommandProc类型的定义为typedef void redisCommandProc(redisClient *c); 
arity
int
命令参数的个数,用于检查命令请求的格式是否正确。 如果这个值为负数 -N ,那么表示参数的数量大于等于N 。 注:命令的名字本身也是一个参数,SET msg "helloworld"命令的参数是"SET""msg""hello world", 而不仅仅是"msg""helloworld"
sflags
char *
字符串形式的标识值, 这个值记录了命令的属性, 比如这个命令是写命令还是读命令, 这个命令是否允许在载入数据时使用, 这个命令是否允许在 Lua 脚本中使用, 等等
flags
int
对 sflags 标识进行分析得出的二进制标识, 由程序自动生成。 服务器对命令标识进行检查时使用的都是 flags 属性而不是 sflags 属性, 因为对二进制标识的检查可以方便地通过 & 、 ^ 、 ~ 等操作来完成
calls
long long
服务器总共执行了多少次这个命令
milliseconds
long long
服务器执行这个命令所耗费的总时长
sflags 属性的标识:
标识
意义
带有这个标识的命令
w
这是一个写入命令,可能会修改数据库
SET 、 RPUSH 、 DEL ,等等
r
这是一个只读命令,不会修改数据库
GET 、 STRLEN 、 EXISTS ,等等
m
这个命令可能会占用大量内存, 执行之前需要先检查服务器的内存使用情况, 如果内存紧缺的话就禁止执行这个命令
SET 、 APPEND 、 RPUSH 、 LPUSH 、 SADD 、SINTERSTORE ,等
a
这是一个管理命令
SAVE 、 BGSAVE 、 SHUTDOWN ,等等
p
这是一个发布与订阅功能方面的命令
PUBLISH 、 SUBSCRIBE 、 PUBSUB ,等等
s
这个命令不可以在 Lua 脚本中使用
BRPOP 、 BLPOP 、 BRPOPLPUSH 、 SPOP ,等等
R
这是一个随机命令, 对于相同的数据集和相同的参数, 命令返回的结果可能不同
SPOP 、 SRANDMEMBER 、 SSCAN 、 RANDOMKEY ,等等
S
Lua脚本中使用这个命令时, 对这个命令的输出结果进行一次排序, 使得命令的结果有序
SINTER 、 SUNION 、 SDIFF 、 SMEMBERS 、KEYS ,等等
l
这个命令可以在服务器载入数据的过程中使用
INFO 、 SHUTDOWN 、 PUBLISH ,等等
t
这是一个允许从服务器在带有过期数据时使用的命令
SLAVEOF 、 PING 、 INFO ,等等
M
这个命令在监视器(monitor)模式下不会自动被传播(propagate)
EXEC
 
redisCommand 结构:
notion image
  • SET命令的名字为"set", 实现函数为setCommand; 命令的参数个数为3, 表示命令接受三个或以上数量的参数; 命令的标识为"wm" , 表示 SET 命令是一个写入命令, 并且在执行这个命令之前, 服务器应该对占用内存状况进行检查, 因为这个命令可能会占用大量内存。
  • GET命令的名字为"get", 实现函数为getCommand函数; 命令的参数个数为2, 表示命令只接受两个参数; 命令的标识为 "r" , 表示这是一个只读命令。
 
当程序以argv[0] 作为输入, 在命令表中进行查找时, 命令表将返回"set"键所对应的redisCommand 结构, 客户端状态的 cmd 指针会指向这个 redisCommand 结构:
notion image
命令名字的大小写不影响命令表的查找结果。因为命令表使用的是大小写无关的查找算法, 无论输入的命令名字是大写、小写或者混合大小写, 只要命令的名字是正确的, 就能找到相应的 redisCommand 结构。比如说, 无论用户输入的命令名字是 "SET" 、 "set" 、 "SeT" 又或者 "sEt" , 命令表返回的都是同一个 redisCommand 结构。这也是 Redis 客户端可以发送不同大小写的命令, 并且获得相同执行结果的原因:
 

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

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

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

在前面的操作中, 服务器已经将要执行命令的实现保存到了客户端状态的 cmd 属性里面, 并将命令的参数和参数个数分别保存到了客户端状态的 argv 属性和 argc 属性里面, 当服务器决定要执行命令时, 它只要执行以下语句就可以了:
因为执行命令所需的实际参数都已经保存到客户端状态的 argv 属性里面了, 所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。
notion image
执行语句:client->cmd->proc(client); 等于执行语句:setCommand(client);
被调用的命令实现函数会执行指定的操作, 并产生相应的命令回复, 这些回复会被保存在客户端状态的输出缓冲区里面(buf 属性和 reply属性), 之后实现函数还会为客户端的套接字关联命令回复处理器, 这个处理器负责将命令回复返回给客户端。
函数调用setCommand(client);将产生一个 "+OK\r\n" 回复, 这个回复会被保存到客户端状态的 buf 属性里面:
notion image

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

在执行完实现函数之后, 服务器还需要执行一些后续工作:
  • 如果服务器开启了慢查询日志功能, 那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长, 更新被执行命令的 redisCommand 结构的 milliseconds 属性, 并将命令的 redisCommand 结构的 calls计数器的值增一。
  • 如果服务器开启了 AOF 持久化功能, 那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面。
  • 如果有其他从服务器正在复制当前这个服务器, 那么服务器会将刚刚执行的命令传播给所有从服务器。
当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落了, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。
 

将命令回复发送给客户端

命令实现函数会将命令回复保存到客户端的输出缓冲区里面, 并为客户端的套接字关联命令回复处理器, 当客户端套接字变为可写状态时, 服务器就会执行命令回复处理器, 将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后, 回复处理器会清空客户端状态的输出缓冲区, 为处理下一个命令请求做好准备。
 
当客户端的套接字变为可写状态时, 命令回复处理器会将协议格式的命令回复 "+OK\r\n" 发送给客户端
 
 

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

当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式, 并打印给用户观看(假设我们使用的是 Redis 自带的redis-cli 客户端):
notion image
当客户端接到服务器发来的 "+OK\r\n" 协议回复时, 它会将这个回复转换成 "OK\n" , 然后打印给用户看:
 
 

serverCron 函数

serverCron函数默认每隔 100ms 执行一次,负责管理服务器资源与保持服务器自身良好运转。
serverCron 函数所做操作:
  • 更新服务器时间缓存
    • Redis服务器有不少功能需要获取系统当前时间,每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
      serverCron函数默认每隔 100ms 更新服务器状态的 unixtime属性和 mstime属性,所以精度不高,用于:打印日志、更新 LRU 时钟、决定是否执行持久化任务、服务器上线时间等对时间精度不高的功能。对于为键设置过期时间,添加慢查询日志等需要高精度的功能,服务器还是会执行系统调用,获得最准确的系统当前时间。
  • 更新 LRU 时钟
    • 服务器状态中的lruclock属性保存了服务器的LRU时钟:
      每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:
      更新服务器状态的 lruclock(10秒更新一次) 和 lru 属性。前者用于计算键的空转时间,后者保存了对象最后一次被命令访问的时间。空转时间=lruclock-lru
  • 更新服务器每秒执行命令次数
    • 调用 trackOperationsPerSecond 函数。以抽样计算的方式,估算并记录服务器在最近 1s 处理的命令请求数量
  • 更新服务器内存峰值函数
    • 更新stat_peak_memory 属性,该属性记录了服务器的内存峰值大小:
      每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量,并于stat_peak_memory 保存的数值对比,如果当前使用的内存数量比stat_peak_memory 属性记录的值要大,那么程序就将当前使用的内存数量记录到stat_peak_memory 属性中。
      info memory 命令查看记录了的服务器的内存峰值:
  • 处理 SIGTERM 信号
    • 在启动服务器时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识:
      每次serverCron函数运行时,程序都会对服务器状态的shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器:
  • 管理客户端资源
    • 调用 clientsCron 函数。如果客户端与服务器之间的连接已经超时,则释放客户端。如果客户端输入缓冲区大小超过一定长度,则释放客户端当前的输入缓冲区,并创建一个默认大小的输入缓冲区;
  • 管理数据库资源
    • 调用 databasesCron 函数,删除过期键,对字典进行收缩操作;
  • 执行被延迟的 BGREWRITEAOF
    • 检查在执行 BGSAVE 命令期间,是否有 BGREWRITEAOF 命令被延迟执行。服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF 命令:
  • 检查持久化操作的运行状态
    • 检查 rdb_child_pid(记录 BGSAVE 命令的子进程 ID) 属性与 aof_child_pid(记录执行 BGREWRITEAOF 命令的子进程 ID) 属性。值为 -1 说明服务器没有进行持久化操作;
      notion image
  • 将 AOF 缓冲区中的内容写入 AOF 文件
    • 如果服务开启 AOF 功能,并且 AOF 缓冲区里有待写入数据,则将 AOF 缓冲区中的内容写入 AOF 文件里
  • 关闭异步客户端
    • 关闭输出缓冲区大小超过限制的客户端
  • 增加 cronloops 计数器的值
    • 对服务器状态的 cronloops 属性增 1
 
 

初始化服务器

一个Redis服务器从启动到能够接受客户端的命令请求,需要进过一系列的初始化和设置过程,比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等。

初始化服务器状态结构

初始化服务器的第一步就是创建一个struct redisServer 类型的实例变量server 作为服务器的状态,并为结构中的各个属性设置默认值。
初始化的工作由 redis.c/initServerConfig 函数完成,这个函数最开头的一 部分代码:
 
该函数的主要工作有:
  • 设置服务器的运行 ID;
  • 设置服务器的默认运行频率;
  • 设置服务器的默认配置文件路径;
  • 设置服务器的运行架构;
  • 设置服务器的默认端口号;
  • 设置服务器的默认 RDB 持久化条件和 AOF 持久化条件;
  • 初始化服务器的 LRU 时钟
  • 创建命令表
 
initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、 慢查询日志、Lua环境、共享对象这些数据结构在之后的步骤才会被创建出来。
initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段——载入配置选项

载入配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。在终端中输入,通过给定配置参数的方式,修改服务器的运行端口号:redis-server --port 10086
 
  • 如果用户未这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值来更新相应的属性
  • 如果用户没有为这些属性的相应选项设置新的值,那么服务器就沿用之前的initServerConfig函数为属性设置的默认值
 

初始化服务器数据结构

负责初始化数据结构,调用 initServer 函数,初始化下列数据库:
设置数据库:server.clients 链表(存客户端)、server.db 数组(存数据库)、server.pubsub_channels 字典(保存频道订阅信息)、server.lua(用于执行 Lua 脚本的 Lua 环境)、server.slowlog(用于保存慢查询日志)
 
调用initServer函数,为数据结构分配内存,并在有需要时,为这些数据结构设置或者关联初始化值。
服务器必须先载入用户指定的配置选项, 然后才能正确地对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进 行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就 要重新调整和修改已创建的数据结构。为了避免出现这种麻烦的情况,服务器选择了将 server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构。
 
除了初始化数据结构之外,initServer还进行了一些非常重要的设置操作
  • 为服务器设置进程信号处理器
  • 创建共享对象(如 OK、整数 0-9999)
  • 打开服务器的监听端口,为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接
  • serverCron 函数创建时间事件
  • 当 AOF 持久化功能打开时,打开现有 AOF 文件或创建并打开一个新的 AOF 文件,为 AOF 写入做准备
  • 初始化服务器的后台 I/O 模块,为将来 I/O 操作做准备
 

还原数据库状态

在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态
根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:
  • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态
  • 相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态
 
成功还原的日志信息: [8040] 01 Dec 20:12:41.758 * DB loaded from disk: 0.001 seconds
 

执行事件循环

打印下列日志后执行事件循环(loop函数); [8040] 01 Dec 20:12:41.758 * The server is now ready to accept connections on port 6379
  • Redis
  • 客户端主从复制
    目录