Redis
服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接受并处理客户端发送的命令请求,并向客户端返回命令回复。
通过使用由I/O
多路复用技术实现的文件事件处理器,Redis
服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient
结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包括
- 客户端的套接字描述符
- 客户端的名字
- 客户端的标志值
- 指向客户端正在使用的数据库的指针,以及该数据库的号码
- 客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
- 客户端的输入缓冲区和输出缓冲区。
- 客户端的复制状态信息,以及进行复制所需要的数据结构。
- 客户端执行
BRPOP
、BLPOP
等列表命令时使用的数据结构。 - 客户端的事务状态,以及执行
WATCH
命令时用到的数据结构。 - 客户端执行发布与订阅功能时用到数据结构。
- 客户端的身份验证标志。
- 客户端的创建时间,客户端和服务器最后一次通信时间,以及客户端的输出缓冲区大小超出软性限制(
soft limit
)的时间
Redis
服务器状态结构clients
属性是一个链表,这个链表保存了所有域服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients
链表来完成。
13.1 客户端属性
客户端状态包含的属性可以分为两类
- 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,他们都要用到这些属性。
- 另外一类是和特定功能相关的属性,比如操作数据库时需要用到的
db
属性和dictid
属性,执行事务时需要用到的mstate
属性,以及执行WATCH
命令时需要用到的watched_keys
属性等等。
13.1.1 套接字描述符
客户端状态的fd
属性记录了客户端正在使用套接字描述符:
typedef struct redisClient{
// ...
int fd;
// ...
}redisClient;
根据客户端类型的不同,fd
属性的值可以是-1
或者是大于-1
的整数
- 伪客户端(
fake client
)的fd
属性的值为-1
:伪客户端处理的命令请求来源于AOF
文件或者Lua
脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。目前Redis
服务器会在两个地方用到为客户端,一个用于载入AOF
文件并还原数据库状态,而另一个则用于执行Lua
脚本中包含的Redis
命令。 - 普通客户端的
fd
属性的值为大于-1
的整数:普通客户端使用套接字来与服务器进行通信,所以服务器会用fd
属性来记录客户端套接字的描述符。因为合法的套接字描述符不能是-1
,所以普通客户端的套接字描述符的值必然是大于-1
的整数。
13.1.2 名字
在默认情况下,一个连接到服务器的客户端是没有名字的。
使用CLIENT setname
命令可以为客户端设置一个名字,然给客户端的身份变得更为清晰。
客户端的名字记录在客户端状态的name
属性里面:
typedef struct redisClient{
// ...
robj *name;
// ...
}redisClient;
如果客户端没有为自己设置名字,那么响应客户端状态的nam
e属性指向NULL
指针;相反的,如果客户端为自己设置了名字,那么name
属性将指向一个字符串对象,而该对象就保存着客户端的名字。
13.1.3 标志
客户端的标志属性flags
记录了客户端的角色(role
),以及客户端目前所处的状态:
typedef struct redisClient{
// ...
int flags;
// ...
}redisClient;
flags属性的值可以是单个标志:
flags=<flag>
也可以是多个标志的二进制或
flags=<flag1>|<flag2>|...
13.1.4 输入缓存区
客户端状态的输入缓冲区用于保存客户端发送的命令请求:
typedef struct redisClient{
// ...
sds querybuf;
// ...
}redisClient;
输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但它的最大大小不能超过1GB
,否者服务器将关闭这个客户端。
13.1.5 命令与命令参数
在服务器将客户端发送吃的命令请求保存到客户端状态querybuf
属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态argv
属性和argc
属性:
typedef struct redisClient{
// ...
robj **argv;
int argc;
// ...
}redisClient;
argv
属性是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]
是要执行的名,而之后的其他项是传给命令的参数。
argc
属性则负责记录argv
数组的长度。
13.1.6 命令的实现函数
当服务器从协议内容中分析并得出argv
属性和argc
属性的值之后,服务器将根据项argv[0]
的值,在命令表中查找命令所对应的命令实现函数。
当程序在命令中成功找到argv[0]
所对应的redisCommand
结构时,它会将客户端状态的cmd
指向这个结构:
typedef struct redisClient{
// ...
struct redisCommand *cmd;
// ...
}redisClient;
之后,服务器就可以使用cmd
属性所指向的redisCommand
结构,以及argv
,argc
属性中所保存的命令参数信息,调用命令实现函数,执行客户端指定的命令。
13.1.7 输出缓冲区
执行命令所得到命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是规定的,另一个缓冲区的大小是可变的:
- 固定大小的缓冲区用于保存那些长度比较小的回复,比如
OK
、简短的字符串值、整数值、错误回复等等。 - 可变大小的缓冲区用于保存那些长度比较大的回复。
客户端的固定大小缓冲区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
,为16KB
当buf
数组的空间已经用完,或者回复太大没有办法放进buf
数组里面时,服务器就会开始使用可变大小缓冲区。
可变大小缓冲区由reply
链表和一个或多个字符串对象组成:
typedef struct redisClient{
// ...
list *reply;
// ...
}redisClient;
通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复。
13.1.8 身份验证
客户端状态的authenticated
属性用于记录客户端是否通过了身份验证:
typedef struct redisClient{
// ...
int authenticated;
// ...
}redisClient;
如果authentication
的值为0
,那么表示客户端未通过身份验证;如果authentication
的值为1
,那么小时客户端已经通过了身份验证。
当客户端authentication
属性的值为0时,除了AUTH
命令之外,客户端发送的所有其他命令都会被服务器拒绝执行。
当客户端通过AUTH
命令成功进行身份验证之后,客户端状态authentication
属性的值就会从0
变成1
,可以正常发送命令请求
authentication
属性仅在服务器启用了身份验证功能时使用。如果服务器没有启用身份验证功能的话,那么authentication
属性的值为0
,服务器也不会拒绝执行客户端发送的命令请求。
13.1.9 时间
typedef struct redisClient{
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
}redisClient;
ctime
属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒
lastinteraction
属性记录了客户端与服务器最后一次进行互动的时间。
obuf_soft_limit_reached_time
属性记录输出缓存区第一次到达软限制的时间。
13.2 客户端的创建与关闭
服务器使用不同的方式来创建和关闭不同类型的客户端。
13.2.1 创建普通客户端
如果客户端通过网络连接与服务器进行连接的普通用户客户端,那么在客户端使用connect
函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients
链表的末尾。
13.2.2 关闭普通客户端
一个普通客户端可以因为多种原因而被关闭:
- 客户端进程退出或者被杀死
- 客户端发送不符合协议格式的命令请求
- 客户端成为了
CLIENT KILL
命令的目标 - 设置了
timeout
配置选项,同时客户端的空转时间超过这个值。 - 客户端输入缓冲区超过限制大小
- 客户端输出缓冲区超过限制大小
服务器使用两种模式来限制客户端输出缓冲区大小:
- 硬性限制:超过硬性限制,立即关闭
- 软性限制:超过软性限制,没有超过硬性限制。使用
obuf_soft_limit_reached_time
记录时间,如果一直超过限制,并且持续时间超过服务器设定的市场,那么服务器关闭客户端;相反,在规定的时间内,没有超过软性限制,那么客户端不会被关闭,同时obuf_soft_limit_reached_time
会被清零。
使用client-output-buffer-limit
选项可以为普通客户端、从服务器客户端。执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
13.2.3 Lua脚本的伪客户端
服务器会在初始化时创建负责执行Lua
脚本中包含的Redis
命令的伪客户端,并将这个伪客户端关联在服务器状态结构lua_client
属性中:
struct redisServer{
// ...
redisClient *lua_client;
// ...
};
lua_client
伪客户端在服务器运行的整个生命期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭。
13.2.4 AOF文件的伪客户端
服务器在载入AOF
文件时,会创建用于执行AOF
文件包含的Redis
命令的伪客户端,并在载入完成之后,关闭这个伪客户端。