0%

Redis中的客户端与服务器实现

Redis服务器可以与多个客户端建立网络连接,每个客户端也可以向服务器发送命令请求。Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

客户端

客户端结构

每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
typedef struct redisClient {
// 套接字描述符
// -1代表伪客户端,来自于AOF文件或者LUA脚本
// 普通客户端为大于-1的整数,记录套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 查询缓冲区
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
// 参数数量
int argc;
// 参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 请求的类型:内联命令还是多条命令
int reqtype;
// 剩余未读取的命令内容数量
int multibulklen; /* number of multi bulk arguments left to read */
// 命令内容的长度
long bulklen; /* length of bulk argument in multi bulk request */
// 回复链表
list *reply;
// 回复链表中对象的总大小
unsigned long reply_bytes; /* Tot bytes of objects in reply list */
// 已发送字节,处理 short write 用
int sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
// 创建客户端的时间
time_t ctime; /* Client creation time */
// 客户端最后一次和服务器互动的时间
time_t lastinteraction; /* time of the last interaction, used for timeout */
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
// 客户端状态标志
// 记录客户端的角色以及客户端所处状态
//
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated; /* when requirepass is non-NULL */
// 复制状态
int replstate; /* replication state if this is a slave */
// 用于保存主服务器传来的 RDB 文件的文件描述符
int repldbfd; /* replication DB file descriptor */
// 读取主服务器传来的 RDB 文件的偏移量
off_t repldboff; /* replication DB file offset */
// 主服务器传来的 RDB 文件的大小
off_t repldbsize; /* replication DB file size */
sds replpreamble; /* replication DB preamble. */
// 主服务器的复制偏移量
long long reploff; /* replication offset if this is our master */
// 从服务器最后一次发送 REPLCONF ACK 时的偏移量
long long repl_ack_off; /* replication ack offset, if this is a slave */
// 从服务器最后一次发送 REPLCONF ACK 的时间
long long repl_ack_time;/* replication ack time, if this is a slave */
// 主服务器的 master run ID
// 保存在客户端,用于执行部分重同步
char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
// 从服务器的监听端口号
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// 阻塞类型
int btype; /* Type of blocking op if REDIS_BLOCKED. */
// 阻塞状态
blockingState bpop; /* blocking state */
// 最后被写入的全局复制偏移量
long long woff; /* Last write global replication offset. */
// 被监视的键
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
// 这个字典记录了客户端所有订阅的频道
// 键为频道名字,值为 NULL
// 也即是,一个频道的集合
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
sds peerid; /* Cached peer ID. */
// 固定大小的缓冲区
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

Redis服务器的clients属性是一个链表,保存了所有客户端的状态。

1
2
3
4
5
6
struct redisServer{
//...
//一个保存所有客户端状态的链表
list* clients;
//...
};

套接字描述符

fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。

如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。

名字

客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。

1
2
3
4
5
6
127.0.0.1:6379> CLIENT LIST
id=5 addr=127.0.0.1:42718 fd=8 name= ...
127.0.0.1:6379> CLIENT SETNAME test-name
OK
127.0.0.1:6379> CLIENT LIST
id=5 addr=127.0.0.1:42718 fd=8 name=test-name ...

如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。

标志

客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。

命令请求

客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。

例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值

1
2
3
4
5
6
7
*3
$3
set
$3
key
$3
value

querybuf的大小最大为1GB,如果超过,Redis会关闭该连接。

在保存到缓冲区之后,会对请求的内容进行分析,并将得出的命令参数和个数保存到argv属性和argc属性中去。 例如对于刚才的命令,argc的值会设置为3,argv会设置为[set,key,value]数组。

当分析出argc和argv之后,服务器会根据argv[0]的值,在命令表中查找对应的命令实现函数,然后将cmd属性指向该命令结构。然后使用cmd属性所指向的redisCommand结构并设置命令参数信息,调用命令实现函数,执行对应的命令。

输出缓冲区

执行命令完成后,命令回复会被保存到输出缓冲区之中。每个客户端都有一个可变大小的缓冲区和固定大小的缓冲区。

其中固定大小的缓冲区有buf和bufpos两个属性组成。buf属性是一个字节数组,bufpos记录了buf数组目前已使用的字节数量。其作用是保存那些长度比较少的回复。

可变大小的缓冲区有reply属性保存。reply是一个链表连接多个字符串对象,用于保存那些长度比较大的回复,例如非常长的字符串,很多项组成的列表,很多元素的集合。

身份验证

authenticated属性记录了客户端是否通过了身份验证。如果其值为0代表未通过身份验证。

客户端的创建与关闭

创建普通客户端

客户端使用connect函数创建连接时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将该客户端状态添加到服务器状态结构clients链表的末尾。

关闭普通客户端

当出现以下情况时会出现客户端关闭:

  • 客户端与服务器的网络连接被关闭。
  • 发送了带有不符合协议格式的命令请求。
  • 客户端成为CLIENT KILL命令的目标
  • 发送的命令请求大小超过了输入缓冲区的大小
  • 命令回复超过了输出缓冲的大小
  • 用户为服务器设置了timeout配置选项。当空转时间超过该选项值时会被关闭。

伪客户端

服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。

服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。

服务器

命令请求过程

发送命令请求

Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。

读取命令请求

当套接字变得可读时,会调用命令处理器来执行以下操作

  • 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。

  • 对输入缓冲区的命令请求进行分析,提取命令参数。

  • 调用命令执行器执行指定的命令。

命令执行器

先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags;
// 实际 FLAG
int flags;
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
// 指定哪些参数是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};

在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。

1
typedef void redisCommandProc(redisClient *c);

在服务器真正实行命令时,只需要调用以下语句即可。

1
client->cmd->proc(client);

被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。

在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。

serverCron函数

Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。

在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。