Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身运转。本文介绍Redis服务器的相关内容。
I、命令请求执行
1.1 发送命令请求
Redis服务器的命令请求来自于客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转化为协议格式,通过套接字发送给服务器:
![](https://img.haomeiwen.com/i10118224/fb63571471f5bae3.png)
1.2 读取命令请求
客户端与服务器之间的连接套接字因为客户端的写入而变为可读时,服务器会调用命令请求处理器来执行以下操作:
1、读取套接字中协议格式的命令请求,并将其保存在客户端状态的输入缓冲区中;
2、对输入缓冲区中的内容进行分析,提取命令参数及参数个数,分别保存在客户端状态的argv和argc属性中;
3、调用命令执行器,执行命令。
下图表示的是一个客户端状态中输入缓冲区:
![](https://img.haomeiwen.com/i10118224/0096b83d356f1b69.png)
完成对输入缓冲区协议的分析之后,客户端状态如下图:
![](https://img.haomeiwen.com/i10118224/3eebda554e53ed48.png)
1.3 命令执行器:查找命令实现
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表(command table)中查找参数所指定的命令,并保存到客户端状态的cmd属性中。
下图展示了command table的样子:
![](https://img.haomeiwen.com/i10118224/bf9edad0522bf0f9.png)
例如执行的如果是set
命令,则在命令表中查找set
,命令表将返回命令所对应的RedisCommand结构,客户端状态的cmd指针会指向这个结构:
![](https://img.haomeiwen.com/i10118224/2a6a4eb8798c6cc1.png)
1.4 命令执行器:执行预备命令
到目前为止,服务器已经将需要执行命令的参数,参数个数,命令实现函数都收集完成,在真正执行命令之前,还需要进行一些预备操作,以确保命令可以正确、顺利被执行。
这些预备操作大概包括:检查cmd指针指向是否有效;检查命令参数个数与格式是否与cmd指向函数相符;检查客户端身份;检查内存情况;等内容。
1.5 命令执行器:调用命令的实现函数
完成上述检查操作确认无误后,服务器通过执行相关命令完成命令的执行,并将结果保存在客户端状态的输出缓冲区(包括buf属性与reply属性)中。
![](https://img.haomeiwen.com/i10118224/1734bef0b8cc616a.png)
然后,实现桉树为客户端的套接字关联回复处理器,将回复返回给客户端。
1.6 命令执行器: 执行后续工作
服务器所需执行的后续操作如进行AOF持久化命令的写入,服务器复制功能命令的传播等。
最后,客户端接收命令并打印显式给用户:
![](https://img.haomeiwen.com/i10118224/92ae30bd9411aa3b.png)
II、serverCorn函数
Redis服务器中的serverCorn函数默认每100毫秒运行一次,这个函数负责管理服务器资源,并保持服务器自身良好运转。
2.1 更新服务器时间缓存
因为Redis服务器有很多功能需要获取系统的当前时间,而每次获取时间都需要一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间缓存:
struct redisServer {
//保存秒级精度的UNIX时间戳
time_t unixtime;
//保存毫秒级精度的UNIX时间戳
long long mstime;
};
因为serverCorn函数每100毫秒更新一次unixtime属性和mstime属性,所以这两个属性的精度并不高:
· 服务器只会在打印日志、更新服务器LRU时钟、决定是否执行持久化任务、计算服务器上线时间等这些对时间精度要求不高的功能上使用这两个属性。
· 对于例如设置过期时间、添加慢查询日志这种对时间精度要求高的功能来说,服务器还是需要进行系统调用,获取准确的系统时间。
2.2 更新LRU时钟
服务器状态中的lruclock属性保存了服务器的LRU时钟,同属于服务器时间缓存的一种。
struct redisServer {
//默认每10秒更新一次
//用于计算键的空转(idle)时长
unsigned lruclock:22;
} redisServer;
每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:
typedef struct redisObject {
unsigned lru::22;
} robj;
当服务器需要计算一个数据库键的空转时间,程序会用lruclock-lru属性,得出计算结果。
2.3 更新服务器每秒执行命令次数
serverCorn函数中的trackOperationsPersecond
函数会以每100毫秒的频率执行,这个函数的功能是以抽样计算的方式,估计并记录服务器在最近一秒钟处理命令请求数量,这个值可以通过INFO status
命令查看。
trackOperationsPerSecond
函数和服务器状态中的四个ops_sec_
开头的属性有关:
struct redisServer {
//上一次进行抽样的时间
long long ops_sec_last_sample_time;
//上一次抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
//REDIS_OPS_SEC_SAMPLES大小的环形数组
/数组中每个项都记录了一次抽样结果
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
//ops_sec_samples数组索引值
//每次抽样后将值+1
//在值等于16时,重置为0,从而实现环形数组功能
int ops_sec_idx;
} ;
trackOperationsPerSecond
函数每次运行,都会根据ops_sec_last_sample_time
记录上一次抽样时间和服务器当前时间,以及ops_sec_last_sample_ops
记录的上一次抽样的已执行命令数量和服务器当前的已执行命令数量,计算出两次抽样之间,服务器平均每一毫秒处理了多少个命令请求,再*1000,就得到了服务器在一秒钟时间处理命令请求数量的估计值,这个估计值被作为一个新的数组项存放到环形数组中。
2.4 更新服务器内存峰值记录
服务器状态的stat_peak_memory
属性记录服务器内存峰值大小:
struct redisServer {
//记录内存峰值大小
size_t stat_peak_memory;
};
每次调用serverCorn函数,程序会查看服务器当前使用内存数量,若大于已有stat_peak_memory则更新。
2.5 处理SIGTERM信号
SIGTERM信号用于打开关闭服务器标志:
struct redisServer {
//关闭服务器标识
//值为1时,关闭服务器
int shutdown_assp;
};
服务器在收到SIGTERM信号时,并不会马上关闭,因为服务器要执行最后一次RDB持久化。
2.6 管理客户端资源
serverCorn函数每次执行都会调用clientsCorn函数,clientsCorn函数会对一定数量的客户端进行一下两个检查:
1、 如果客户端与服务器之间的连接已经超时,则程序释放客户端;
2、 如果客户端在一次执行命令之后,输入缓冲区超多了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存。
2.7 管理数据库资源
serverCorn函数会每次调用databasesCorn函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作。
2.8 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE
命令期间,如果客户端向服务器发来了BGREWRITEAOF
命令,则服务器会将 BGREWRITEAOF
命令的执行时间延迟到BGSAVE
执行完毕之后。
服务器中的aof_rewrite_scheduled
标识记录了服务器是否延迟了BGREWRITEAOF
命令:
struct redisServer {
//如果为1,则说明被延迟
int aof_rewrite_scheduled;
};
2.9 检查持久化操作的运行状态
服务器状态使用rdb_child_pid
属性和aof_child_pid
属性记录执行BGSAVE
和BGREWRITEAOF
命令的子进程ID,这两个属性也可以用于检查两个命令是否在执行:
struct redisServer {
//记录执行BGSAVE命令的子进程ID
//如果没有正在执行,则值为-1
pid_t rdb_child_pid;
pid_t aof_child_pid;
};
每次serverCorn函数执行时,都会检查上述两个属性的值,只要其中一个不为-1,程序就会执行一次wait3
行数,检查子进程是否有信号发来服务器进程:
· 如果有信号到达,说明新的RDB文件已经生成完毕或AOF文件已经重写完毕,服务器可以执行后续操作。
· 如果没有信道到达,表示持久化操作未完成,程序不做动作。
如果两个值都为-1, 说明此时服务器没有在进行持久化操作,则服务器会执行以下判断:
![](https://img.haomeiwen.com/i10118224/7ef53ccdec180007.png)
2.10 将AOF缓冲区内容写入AOF文件
如果服务器开启了AOF持久化功能,并在AOF缓冲区内有待写入的数据,则serverCorn会调用相应程序,将AOF缓冲区中的内容写入到AOF文件中。
2.11 关闭异步客户端
在这一步,服务器会关闭输出缓冲区大小超出限制的客户端。
2.12 增加cronloops计数器的值
服务器状态的cronloops属性记录了serverCorn函数执行的次数。
III、初始化服务器
服务器从启动到能够处理客户端命令请求主要需要执行5个步骤:
1、 初始化服务器状态;
2、 载入服务器配置;
3、 初始化服务器数据结构;
4、 还原数据库状态;
5、执行事件循环;
3.1 初始化服务器状态结构
服务器初始化的第一步就是创建一个struct RedisServer类型的实例变量server作为服务器状态,并为结构中的各个属性设置默认值,这个操作由redis.c/initServerConfig函数完成。
3.2 载入配置选项
服务器在用initServerConfig函数完成初始化之后,就会开始载入用于给定的配置参数和配置文件,并根据用户设定的配置,对server实例中变量相关属性进行修改。
3.3 初始化服务器数据结构
在执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,不过除了命令表外,服务器还包含其他数据结构,如:
· serve.clients链表,记录所有与服务器相连的客户端的状态结构,链表每个节点为一个redisClient结构实力。
· server.db数组,数组中包含了服务器的左右数据库。
· 用于保存频道订阅信息的server.pubsub_channels字典,以及保存模式订阅信息的server.pubsub_patterns链表。
...
当初始化进行到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存。
服务器到现在才初始化数据结构的原因在于: 服务器必须先载入用户指定的配置选项,然后才能正确的对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构,为了避免这种情况,服务器将server状态的初始化分为两步。
除了对数据结构初始化之外,initServer还进行了一些非常重要的设置:
· 为服务器设置进程信号处理器;
· 创建共享对象,如“OK”回复的字符串对象等;
· 打开服务器监听端口;
· 初始化服务器的后台I/O模块;
3.4 还原数据库状态
在完成上述服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,还原服务器数据库状态。
· 如果服务器启动了AOF持久化功能,则服务器使用AOF文件来还原数据库状态。
· 否则,服务器使用RDB文件来还原数据库状态。
3.5 执行事件循环
服务器开启事件循环,接受客户端的连接请求。
【参考】
[1] 《Redis设计与实现》
欢迎转载,转载请注明出处wenmingxing Redis之服务器
网友评论