赞
踩
Redis是一个典型一对多服务器程序,一个服务器可以与多个客户端进行网络连接,每隔客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令请求。
通过是一个I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式处理命令请求,并与多个客户端进行网络通信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要的数据结构,其中包括:
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过表里clients链表来完成。
- struct redisServer
- {
- //......
- list* clients;
- //......
- };
下图展示了一个与三个客户端进行连接的服务器:
下面对客户端状态的各个属性进行介绍,并讲述服务器创建并关闭各种不同类型的客户端的方法。
客户端状态的属性可以分为两类:
下面对客户端状态中比较通用的那部分属性进行介绍。
客户端状态的fd属性记录了客户端正在使用的套接字描述符。
- typedef struct redisClient
- {
- //...
- int fd;
- //...
- }redisClient;
根据客户端类型的不同,fd可以是-1或者大于-1的整数。
执行CLIENT list命令可以列出目前所有连接到服务器的普通客户端,命令输出的fd域显示了服务器连接客户端使用的套接字描述符。
在默认情况下,一个连接服务器的客户端是没有名字的。比如:在上面的执行了CLIENT list命令中,两个客户端的name域都是空白的。
使用CLIENT setname命令可以为客户端这只一个名字,让客户端的身份变得更加清晰。
客户端的名字记录在客户端状态的name属性里面:
- typedef struct redisClient
- {
- //...
- robj* name;
- //...
- }redisClient;
如果客户端没有为自己设置名字,那么相应客户端状态的name属性指向NULL指针,相反的,如果客户端为自己设置了名字,那么name属性将指向一个字符串对象,那该对象就保存客户端的名字。
客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态:
- typedef struct redisClient
- {
- //...
- int flags;
- //...
- }redisClient;
flags属性的值可以是当个标志,也可以是多个表示的二进制或(每一个标志是一个常量,多个标志只需要或起来)。
一部分标志记录了客户端的角色:
另一部分标志则记录了客户端目前所处的状态:
以上所有标志定义都在redis.h文件中。
PUBSUB命令和SCRIPT LOAD命令的特殊性:
通常情况下,Redis只会将那些对数据库更改的命令写入到AOF文件,并复制到各个从服务器。如果一个命令没有对数据库进行任何修改,那它认为是只读命令,这个命令不会被写入到AOF文件中,也不会被复制到从服务器。
以上规则适用于绝大部分Redis命令,当PUBSUB命令和SCRIPT LOAD命令是其中的例外。PUBSUB命令虽然没有修改数据库,当PUBSUB命令向频道的所有订阅者发送消息这一行为有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变。因此,服务器需要使用REDIS_FORCE_AOF标志,强制将这个命令写入AOF文件,这样在载入AOF文件时,服务器就可以再次执行相同的PUBSUB命令,并产生相同的作用。
SCRIPT LOAD命令的情况与PUBSUB命令类似,虽然SCRIPT LOAD命令没有修改数据库,但它修改了服务器状态,所以它是一个带有副作用的命令,服务器需要使用REDIS_FORCE_AOF标志,强制将这个命令写入AOF文件,使得将来在载入AOF文件时,服务器可以产生相同的副作用。
另外,为了让主服务器和从服务器都可以正确地载入SCRIPT LOAD命令指定地脚本,服务器需要使用REDIS_FORCE_REPL标志,强制将SCRIPT LOAD命令复制给所有服务器。
客户端状态地输入缓冲区用于保存客户端发送地命令请求:
- typedef struct redisClient
- {
- //...
- sds querybuf;
- //...
- }redisClient;
举个例子,如果客户端向服务器发送set key value的请求,那么客户端状态的querybuf属性如下图:
输入缓冲区的大小会根据输入内容动态的缩小或者扩大,但它的最大大小不能超过1GB,否则服务器会关闭这个客户端。
在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器对命令请求进行解析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv和argc属性中。
- typedef struct redisClient
- {
- //...
- robj **argv;
-
- int argc;
- //...
-
- }redisClient;
argv是一个数组,数组中的每一个项都是一个字符串对象,其中argv[0]是要执行的命令,而之后的其他项是传给命令的参数。
argc属性则负责记录argv数组的长度。
举个例子:对于set key value命令,argv和argc属性如下图:
注意,argc属性的值是3不是2,因为"set"本身也是参数。
当服务器从协议内容中分析并得到argv属性和argc属性值后,服务器会根据argv[0]的值,在命令表中查找命令所对应的实现函数。
命令表是一个字典,字典的键是一个SDS结构,保存了命令的名字,字典的值是命令所对应的redisCommand结构,这个结构保存了命令实现函数,命令的标志,命令应该给定的参数个数,命令的总执行次数和总消耗时长等统计信息。下图展示了命令表实例:
当程序在命令表中找到argv[0]所对应的redisCommand结构时,它会将客户端状态cmd指向结构。
- typedef struct redisClient
- {
- //...
- struct redisCommand* cmd;
- //...
- }redisClient;
之后服务器可以使用cmd属性所指向的redisCommand结构,以及argv,argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的命令。
下图演示了服务器argv[0]为"SET"时,查找命令表并将客户端状态cmd指针指向目标redisCommand结构的整个过程。
针对命令表的查找操作不区分字母的大小写,查找结果都是一样的。
执行的命令回复会被保存在输出缓冲区中,每个客户端都会有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的。
客户端固定大小缓冲区由buf和bufpos两个属性组成:
- typedef struct redisClient
- {
- //...
- char buf[REDIS_REPLY_CHUNK_BYTES];
- int bufpos;
- //...
-
- }redisClient;
buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组,而bufpos属性则记录了buf数组目前已使用的字节数量。
REDIS_REPLY_CHUNK_BYTES常量目前的默认值为16*1024,也就是buf数组的默认大小为16KB。
当buf数组的空间已经用完,或者回复因为太大而没办法放进buf数组里面时,服务器就会开始使用可变大小缓冲区。
下图展示了一个使用固定大小缓冲区来保存返回值+OK\r\n的例子。
可变大小缓冲区由reply链表和一个或多个字符串对象组成。
- typedef struct redisClient
- {
- //...
- list* reply;
- //...
- }redisClient;
通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复。
下图展示了一个包含三个字符串对象的reply链表。
客户端状态的authenticated属于用于记录客户端是否通过身份验证:
- typedef struct redisClient
- {
- //...
- int authenticated;
- //...
- }redisClient;
如果authenticated的值为0,那么表示客户端未通过身份验证,如果authenticated的值为1,表示客户端通过了身份验证。
举个例子:
对于一个尚未进行身份验证的客户端来说,客户端状态的authenticated属性将如下图。
当客户端 authenticated属性的值为0时,处理AUTH命令之外,客户端发送的所有命令都会被服务器拒绝执行:
当客户端通过AUTH命令成功进行身份验证之后,客户端状态 authenticated属性的值从0变为1,这时客户端就可以向往常一样向服务器发送请求了:
authenticated属性仅在服务器启用了身份验证功能时使用,如果服务器没有启用身份验证功能,那么即使authenticated属性值为0(默认值),服务器也不会拒绝执行客户端发送的命令请求。
配置为redis.conf的requirepass字段。
客户端还有几个和时间有关的属性。
- typedef redisClient
- {
- //...
- time_t ctime; /* Client creation time. */
- time_t lastinteraction; /* Time of the last interaction, used for timeout */
- time_t obuf_soft_limit_reached_time;
- //...
- }redisClient;
ctime属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,CLIENT LIST命令的age域记录了这个描述。
lastinteraction属性记录了客户端与服务器最后一次互动的时间,这里的互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。
lastinteraction属性可以用来计算客户端的空转时间,也就是,距离客户端与服务器最后一次进行互动以来,已经过去了多少秒,CLIENT LIST命令的idle域记录了这个秒数。
obuf_soft_limit_reached_time属性记录了输出缓冲区第一次到达软性限制的时间。
服务器使用不同的方式来创建和关闭不同类型的客户端。
如果客户端是通过网络连接于服务器进行连接的普通客户端,那么在客户端使用connect函数连接服务器的时候,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新客户端状态添加到服务器状态结构clients链表末尾。
举个例子:假设当前有c1和c2两个普通客户端正在连接服务器,那么当一个新的普通客户端c3连接服务器之后,服务器会将c3所对应的客户端状态添加到clients链表的末尾,如下图:
一个普通客户端可以因为多种原因而被关闭:
前面介绍输出缓冲区的时候提到过,可变大小缓冲区由一个链表和任意多个字符串对象组成,理论上来说,这个缓冲区可以保存任意长的命令回复。
但是,为了避免客户端的回复过大,占用过多的服务器资源,服务器会时刻检查客户单的输出缓冲区大小,并在缓冲区的大小超出范围时,执行相应的限制操作。
服务器使用两种模式来现在客户端输出缓冲区大小:
使用client-output-buffer-limit选项可以为普通客户端,从服务器客户端,执行发布与订阅功能的客户端设置不同的软性限制和硬性限制,该选项的格式为:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
示例:
第一行设置将普通客户端的硬性限制和软性限制都设置为0,表示不限制客户端的输出缓冲区大小。
第二行设置将从服务器客户端的硬性限制设置为256MB,而软性限制设置为64MB,软性限制的时长为60秒。
第三行设置将自己洗干嘛发布与订阅功能的客户端的硬性限制设置为32MB,软性限制设置为8MB,软性限制的时长为60秒。
服务器在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中:
- struct redisServer
- {
- //...
- redisClient* lua_client;
- //...
- };
lua_client伪客户端在服务器运行的整个生命周期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭。
服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成之后,关闭这个伪客户端。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。