Redis是典型的一对多服务器程序,一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,服务器可以处理请求并回复。

redis中,所有的客户端信息都保存在redisServer的clients结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct redisServer {
// 一个数组,保存着服务器中所有的数据库
redisDb *db;

// 服务器数据库的数量
int dbnum;

// 记录了保存条件的数组
struct saveparam *saveparam;

// 修改计数器
long long dirty;

// 上一次执行保存的时间
time_t lastsave;

// AOF缓冲区
sds aof_buf;

// 一个链表,保存了所有客户端状态
list *clients;
};

一个具体的结构如下:

image-20230327142807149

客户端属性

客户端属性分为两类:

一类是比较普通的属性,一类是和特定功能相关的属性。

套接字描述符

客户端状态的fd属性记录了客户端正在使用的套接字描述符:

1
2
3
4
5
6
7
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

} redisClient;

根据客户端的种类不同,fd可以为-1 或者大于-1的整数。

伪客户端的fd属性为-1,它处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接。有两个地方会用到,一个用于载入AOF文件,另一个用于执行Lua脚本中包含的Redis命令。

普通客户端的fd属性为大于-1的整数,普通客户端使用套接字来与服务器进行通信,服务器用fd属性来记录客户端套接字。

名字

1
2
3
4
5
6
7
8
9
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;
} redisClient;

标记

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

} redisClient;

输入缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

} redisClient;

一个具体的结构如下:

image-20230327215316020

命令与参数

在服务器将客户端发送的命令请求保存到客户端的querybuf中之后,服务器会将命令的内容进行解析,并将得出命令参数以及命令参数的个数,分别保存到客户端的argv和argc属性当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

robj **argv;

int argc;

} redisClient;

一个具体的例子如下:

image-20230327215550229

命令的实现函数

当服务器从协议内容中分析得出argv和argc的属性之后,服务器会根据argv[0]的值去命令表中查看对应命令的实现。

一个具体的命令表如下所示:

image-20230327220733118

其中redisCommand结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

robj **argv;

int argc;

// 保存指令具体执行过程的结构体
struct redisCommand *cmd;

} redisClient;

查找对具体的命令对应的操作之后,会把客户端状态的cmd指向该命令所对应的具体执行过程的结构体,也就是上面的redisCommand。

image-20230327222247558

输出缓冲区

执行命令得到的回复会被保存在客户端状态的输出缓冲区里,每个客户端都有两个输出缓冲区,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的。

固定大小的缓冲区用于保存那些长度比较小的回复,比如OK,简短的字符串值,整数,错误恢复等。

可变大小的缓冲区用于保存那些长度比较大的回复,比如一个非常长的字符串值,一个包含很多元素的集合等。

客户端固定大小的缓冲区由buf和bufpos两部分组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

robj **argv;

int argc;

// 保存指令具体执行过程的结构体
struct redisCommand *cmd;

char buf[REDIS_REPLY_CHUNK_BYTES];

int bufpos;
} redisClient;

其中buf是一个字节数组,而bufpos记录了字节数组中已经使用的字节数量。

当buf数组的空间使用完,或者因为回复太大没办法放进去时,就会采用可变大小缓冲区。

可变大小缓冲区由reply链表和一个或多个字符串对象组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

robj **argv;

int argc;

// 保存指令具体执行过程的结构体
struct redisCommand *cmd;

char buf[REDIS_REPLY_CHUNK_BYTES];

int bufpos;

list *reply;
} redisClient;

通过使用链表来链接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复。具体结构如下:

image-20230327223418524

时间

客户端还有几个其他的关键属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef struct redisClient {
// 记录客户端当前正在使用的数据库
redisDb *db;

int fd;

// 用于标记redisClient名字
robj *name;

// 客户端的标志属性flags记录了客户端的角色
int flag;

// 输入缓冲区
sds querybuf;

robj **argv;

int argc;

// 保存指令具体执行过程的结构体
struct redisCommand *cmd;

char buf[REDIS_REPLY_CHUNK_BYTES];

int bufpos;

list *reply;

// 计算客户端与服务器连接了多长时间
time_t ctime;

// 客户端最后一次与服务器互动的时间
time_t lastinteraction;

// 记录了输出缓冲区第一次到达软性限制的时间
time_t obuf_soft_limit_reached_time;
} redisClient;

ctime属性记录了创建客户端的时间,这个时间用于计算客户端与服务器连接了多长时间。

lastinteraction记录了客户端最后一次与服务器互动的时间,这个互动可以是客户端向服务器发送命令,也可以是服务器向客户端发送命令回复。

obuf_soft_limit_reached_time记录了输出缓冲区第一次到达软性限制的时间。

客户端的创建与关闭

创建普通客户端

如果客户端是使用网络连接的普通客户端,那么客户端在使用connect连接到服务器时,服务器会在redisServer的clients属性后多链接一个客户端。如下图所示

image-20230328102150894

关闭普通客户端

一个客户端可以因为多种原因被关闭:

如果客户端进程退出或被杀死,那么客户端与服务器之间的网络连接被关闭,造成客户端被关闭。

如果客户端发送带有不符合协议格式的请求命令,也会被关闭。

如果客户端成为了CLIENT KILL 命令的目标,也会被关闭。

如果用户为服务器设置了timeout属性,那么客户端的空转时间超过timeout选项设置的值,客户端也会关闭。

如果客户端发送的请求命令大于输入缓冲区的大小会被关闭。

如果要发送给客户端的命令回复大小超过了输出缓冲区的大小限制,那么这个客户端也会被关闭。

服务器使用两种模式来限制输出缓冲区大小:

1、硬性限制:如果缓冲区大小超出了硬性限制大小,客户端会被立刻关闭。

2、软性限制:超过软性限制大小但是没超过硬性大小,那么服务器将使用客户端状态obuf_soft_limit_reached_time属性记录客户端到达软性限制的起始时间,之后会监视这个客户端,如果超出软性限制的时间超过了服务器设定的时间,那么客户端会被关闭。

参考

《Redis设计与实现》