第一部分: 介绍Redis客户端-服务器架构;
第二部分:介绍redis客户端,分为三个小节:redis客户端-服务器架构、redis客户端属性和redis客户端创建与关闭;
第三部分:介绍redis服务端,分为两个小节:redis服务端初始化、redis服务端处理命令请求执行过程。
Redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。Redis客户端-服务器的结构如下图:
通过使用I/O多路复用技术实现的文件事件处理器, Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/ redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时所需要的数据结构,主要包括(“6.2 客户端属性”会具体介绍每一个属性):
信息名称 | redisClient属性名称 | 对应下面的小节 | 备注 |
---|---|---|---|
套接字描述符 | fd属性 | 6.2.1 套接字描述符(fd) | 记录了客户端正在使用的套接字描述符 |
名字 | name属性 | 6.2.2 名字(name) | 记录了连接到服务器的客户端名字 |
标志 | flag属性 | 6.2.3 标志(flags) | 记录了客户端的角色及目前所处的状态 |
输入缓冲区 | querybuf属性 | 6.2.4 输入缓冲区(querybuf) | 用户输入和输入缓冲区中的内容是不一样的,如用户输入为SET KEY VALUE,输入缓冲区内容为*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n,下面会介绍。 |
命令与命令参数 | argv属性、argc属性 | 6.2.5 命令与命令参数(argv argc) | argv属性表示数组本身,argc属性表示数组元素个数 |
命令的实现函数 | cmd属性(cmd指针) | 6.2.6 命令的实现函数(redisCommand) | cmd是redisClient中的一个指针属性,redisCommand是命令的具体值 |
输出缓冲区 | buf属性、bufpos属性 | 6.2.7 输出缓冲区(buf bufpos)(固定缓冲区+可变缓冲区) | 输出缓冲区和显示给用户的内容是不一样的,如输出缓冲区的内容为+OK\r\n,显示给用户为OK,下面会介绍。 |
身份验证 | authenticated属性 | 6.2.8 身份验证(authenticated) | 客户端是否通过相互验证,值为0,未通过身份验证,值为1,通过身份验证 |
时间 | ctime属性、lastinteraction属性、obuf_soft_limit_reached_time属性 | 6.2.9 时间(ctime lastinteraction obuf_soft_limit_reached_time) | 客户端、服务器网络交互相关实现 |
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构。对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历client链表来完成,如下图:
客户端状态包含的属性可以分为两类:
一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,它们都要用到这些属性,本节详细介绍。
另外一类是和特定功能相关的属性,比如操作数据库时需要用到的db属性和dictid属性,执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_keys属性等等,不介绍,略过。
先上一张图,redis客户端运行时,输入“client list”打印客户端状态:
客户端状态的fd属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd属性的值可以是-1或者是大于-1的整数 :
1)伪客户端(fake client)的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。目前Redis服务器会在两个地方用到伪客户端,一个用于载入文件井还原数据库状态,而另一个则用于执行Lua脚本中包含的 Redis命令。
2)普通客户端的fd属性的值为大于-1的整数 :普通客户端使用套接字来与服务器进行通信,所以服务器会用fd属性来记录客户端套接字的描述符。因为合法的套接字描述符不能是-1,所以普通客户端的套接字描述符的值必然是大于-1的整数。
在默认情况下,一个连接到服务器的客户端是没有名字的,使用client setname命令可以为客户端设置一个名字,让客户端的身份变得更清晰,如图:
客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态:
typedef struct redisClient{
int flags;
}redisClient;
flags属性中,每个标志使用一个常量表示,一部分标志记录了客户端的角色,如:
(1)在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。 REDIS_MASTER标志表示客户端代表的是一个主服务器, REDIS_SLAVE标志表示客户端代表的是一个从服务器
(2)REDIS_PRE_PSYNC标志表示客户端代表的是一个版本低于Redis2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在 REDIS_SLAVE标志处于打开状态时使用。
(3)REDIS_LUA_ CLIENT标识表示客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端。
而另外一部分标志则记录了客户端目前所处的状态
以上提到的所有标志都定义在redis.h文件里面 | |
---|---|
REDIS_MONITOR标志 | 表示客户端正在执行 MONITOR命令 |
REDIS_MONITOR标志 | 表示服务器使用UNIX套接字来连接客户端 |
REDIS_BLOCKED标志 | 表示客户端正在被 BRPOP、BPOP等命令阻塞 |
REDIS_UNBLOCKED标志 | 表示客户端已经从 REDIS_BLOCKED标志所表示的阻塞状态中脱离出来,不再阻塞, REDIS_UNBLOCKED标志只能在 REDIS_BLOCKED标志已经打开的情况下使用。 |
REDIS_MULTI标志 | 表示客户端正在执行事务。 |
REDIS_ DIRTY_CAS标志 | 表示事务使用 WATCH命令监视的数据库键已经被修改 |
REDIS_DIRTY_EXEC标志 | 表示事务在命令入队时出现了错误 |
REDIS_CLOSE_ASAP标志 | 表示客户端的输出缓冲区大小超出了服务器,服务器会在下一次执行 servercron函数时关闭这个客户端,以免服务器的稳定性受到这个客户端影响。积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端。 |
REDIS_CLOSE_AFTER_REPLY标志 | 表示有用户对这个客户端执行了CLIENT KILL命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端。 |
REDIS_ASKING标志 | 表示客户端向集群节点(运行在集群模式下的服务器)发送了ASKING命令。 |
REDIS_FORCE_AOF标志 | 强制服务器将当前执行的命令写人到AOF文件里面 |
REDIS_FORCE_REPL标志 | 强制主服务器将当前执行的命令复制给所有从服务器 |
输入缓冲区querybuf的大小会根据输入内容动态地缩小或者扩大,但是它的最大大小不能超过1GB,否则服务器关闭这个客户端。
当服务器从协议内容中分析并得到argv属性和argc属性的值之后,redis服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。
typedef struct redisClient{
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
}redisClient;
redisClient属性(与时间相关的属性) | 含义 |
---|---|
ctime | 该属性记录创建客户端的时间,这个时间用来计算客户端与服务器已经连接了多少秒了,使用client list命令查看时,age域记录了这个秒数(age域以秒为单位,记录了ctime参数) |
lastinteraction | 该属性记录了客户端与服务器最后一个进行互动interaction的时间,这里的互动指的是客户端对服务端命令请求和服务端对客户端的命令回复。 |
obuf_soft_limit_reached_time | 该属性用来计算客户端的空转时间idle,即距离客户端与服务端的最后一次交互,已经过去的了多少秒,使用client list命令查看时,idle域记录了这个秒数(idle域以秒为单位,记录了obuf_soft_limit_reached_time参数) |
如果客户端通过网络连接与服务器进行连接的是普通客户端,那么在客户端使用connect函数连接到服务器时,服务器就会调用连续事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
举个例子,假设当前有c1和c2两个普通客户端正在连接服务器,那么当一个新的普通客户端c3连接到服务器之后,服务器会将c3所对应的客户端状态添加到clients链表的末尾,如图:
注意:上图中用虚线包围的就是服务器为c3新创建的客户端状态。
一个普通客户端可以因为多种原因面被关闭:
1)如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
2)如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
3)如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭。
4)如果用户为服务器设置了timeout配置选项,郡么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。
5)如果客户端发送的命令请求的大小超过了输人缓冲区的限制大小(默认为1GB),那么这个客户端会被服务器关闭。
6)如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
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:getRandomHexChars(server,runid,REDIS_RUN_ID_SIZE);
设置服务器的默认运行频率:server.hz=REDIS_DEFAULT_HZ;
设置服务器的默认配置文件路径:server.configfile=null;
设置服务器的运行架构:server.arch_bits=(sizeof(long)==8) ? 64 : 32;
设置服务器的默认端口号:server.port=REDIS_SERVERPORT;
设置服务器的默认RDB持久化条件和AOF持久化条件,初始化服务器的LRU时钟,创建命令表。
initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外, initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lu环境、共享对象这些数据结构在之后的步骤才会被创建出来。
当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段一载人配置选项。
分为两个部分,即“载入用户指定的配置选项+server状态更新”。
(1)载入用户指定的配置选项
在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。举个例子,如果我们在终端中输入:
$ redis-server --port 10086
那么我们就通过给定配置参数的方式,修改了服务器的运行端口号。另外,如果我们在端中输入:
$ redis-server redis.conf
井且redis. conf文件中包含以下内容:
# 将redis服务器的数据库数量设置为32个(默认16个 db0-db15)
database 32
# 关闭RDB文件的压缩功能
rdbcompression no
那么我们就通过指定配
置修改了服务器的数据库数量,以及RDB持久化模块的压缩功能。
(2)server状态更新
Redis服务器在用initServerConfig函数初始化完 server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。
例如,在初始化 server变量时,程序会为决定服务器端口号的port属性设置默认值,为数据库数量设置默认值:
void initServerConfig(void){
server.port = REDIS_SERVERPORT; //默认为6379端口 上面用户设置为10086
server.dbnum = REDIS_DEFAULT_DBNUM; //默认为16个数据库 上面用户设置为32个
}
这里,因为用户在启动服务器时为配置选项port指定了新值10086,dbnum指定新值为32,所以server port属性的值就会被更新为10086,server donum属性的值就会被更新为32,这就是server状态更新;所以,服务器的端口号从默认的6379变为用户指定的10086,数据库数量从默认的16个变为用户指定的32个,这就是让用户指定的配置选项生效。
所以,用户指定配置选项和server状态更新是同时进行的,所以这里放在同一个小节中。
实际上,其他配置选项相关的服务器状态属性的情况与上面列举的port属性和dbnum属性一样,如果用户为这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值来更新相应的属性;同理,如果用户没有为属性的相应选项设置新的值,那么服务器就沿用之前initserverconfig函数为属性设置的默认值。
服务器在载入用户指定的配置选项,井对 server状态进行更新之后,服务器就可以进入初始化的第三个阶段一初始化服务器数据结构。
注意,在Redis服务器的初始化分为两步,initServerConfig函数主要负责初始化一般属性,initServer函数主要负责初始化数据结构(此外,initServer函数还完成一些重要的设置动作)。
(1)initServer函数负责初始化数据结构
初始化server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个 redisClient结构实例;
初始化server.db数组,数组中包含了服务器的所有数据库;
初始化sorver, pubaub channela字典,该字典用于保存频道订阅信息;
初始化server, pubsub patterns链表,该链表用于保存模式订阅信息;
初始化server.lua环境,该环境用于执行Lua脚本;
初始化server.showlog属性,该属性用于保存慢查询日志。
(2)initServer函数负责完成重要的设置动作
为服务器设置进程信号处理器
创建共享对象:这些对象包含 Redis服务器经常用到的一些值,服务器通过重用这些共享对象来避免反复创建相同的对象。
打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。
为serverCron函数创建时间事件 ,等待服务器正式运行时执行serverCron函数。
如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备
初始化服务器的后台I/O模块(bio)为将来的I/O操作做好准备
小结:当initServer函数执行完毕之后,服务器将用ASCII字符在日志中打印出Redis的图标,以及Redis版本信息,如下:
在完成了对服务器状态 server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所口如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态
相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原
当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载人文件并还原数据库状态所耗费的时长
在初始化的最后一步,服务器将打印出以下日志:
并开始执行服务器的事件循环(loop)。
至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求,且看下面的“7.2 命令请求的执行过程”。
一图预览,redis整个命令请求执行过程。
一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。举个例子,如果我们使用客户端执行以下命令:
redis > SET KEY VALUE
OK
那么从客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务器共需要执行以下操作:
1)客户端向服务器发送命令请求 SET KEY VALUE(上图中的“发送命令请求”);
2)服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK(上图中的“读取命令请求、执行命令请求”);
3)服务器将命令回复OK发送给客户端(上图中的“命令回复发送给客户端”);
4)客户端接收服务器返回的命令回复OK,并将这个回复打印给用户查看(上图中的“命令回复发送给客户端”)。
当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器,如下图:
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
1)读取套接字中协议格式的命令请求,并将其保存到客户端状态的输人缓冲区里面。
2)对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和杂数个数保存到客户端状态的argv属性和argc属性里面
3)调用命令执行器,执行客户端指定的命令
步骤一和步骤二在图中演示了,步骤三即“调用命令执行器,执行客户端指定的命令”且看“7.2.3 执行命令请求”。
(1)查找命令实现函数
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表(common table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。注意,这是两个步骤,
1)在命令表(common table)中查找参数所指定的命令
2)将找到的命令保存到客户端状态的cmd属性里面
如图:
(2)执行预备操作
到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:
a.检查客户端状态的cmd指针是否指向NULL,如果是的话,那么说明用户输入命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。
b.根据客户端cmd属性指向的 redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。
c.检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端试图执行除AUH命令之外的其他命令,那么服务器将向客户端返回一个错误。
d.如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返闻一个错误。
e.如果服务器上一次执行 BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgbrave-error功能,而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误
f.如果客户端当前正在用 SUBSCRIBE命令订阅频道,或者正在用 PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的 SURSCRIBE、 PSUBSCRIBE、UNSUBSCRIBE、 PUNSUBSCRIBE四个命令,其他命令都会被服务器拒绝
g.如果服务器正在进行数据载人,那么客户端发送的命令必须带有1标识(比如INFO、 SHUTDOWN、 PUBLSH等等 )才会被服务器执行,其他命令都会被服务器
h.如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和 SCRIPT KILL命令,其他命令都会被服务器拒绝。
i.如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、 DISCARD、MULTI、 WATCH四个命令,其他命令都会被放进事务队列中
j.如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。当完成了以上预备操作之后,服务器就可以开始真正执行命令了
注意:以上只列出了服务器在单机模式下执行命令时的检查操作,当服务器在复制或者集群模式下执行命令时,预备揉作还会更多一些。
(3)调用命令实现函数
在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端状态的argv属性和argc属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:
因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。如图:
(4)执行后续工作
在执行完实现函数之后,服务器还需要执行一些后续工作:
a.如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
b.根据刚刚执行命令所耗费的时长,更新被执行命令的 redisCommand结构的milliseconde属性,并将命令的redisCommand结构的ca11s计数器的值增一
c.如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写人到AOF缓冲区里面
d.如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器
当以上操作都执行完了之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里面(buf属性和 reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端.
对于前面SET命令的例子来说,函数调用 setCommand(client)将产生一个 ”+OK r\n“回复,这个回复会被保存到客户端状态的buf属性里面,如图:
当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看(假设我们使用的是Redis自带的reds-cli客户端),如图148所示:
执行命令请求整个过程,且看图1:
实际上,对于“步骤3执行命令请求”的四个步骤3.1 3.2 3.3 3.4 可以画在一个图中,得到图2:
将步骤2、步骤3、步骤4放在一起,得到图3:
至此,Redis客户端和服务端整个命令执行过程完毕。
Redis客户端-服务端架构,完成了。