Redis源码剖析——客户端和服务器

Redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接。这篇文章将通过源码看看客户端和服务器的底层数据结构和工作过程

在Redis这种一对多的服务模式下,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

客户端

客户端数据结构

客户端底层的数据结构如下:

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
typedef struct redisClient {
uint64_t id; /* Client incremental unique ID. */
// 套接字描述符
int fd;
redisDb *db;
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 */
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 ... */
// 标志是否通过身份验证
int authenticated; /* when requirepass is non-NULL */
... // 其他相关属性

/* Response buffer */
// 回应缓冲区
int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

在客户端的各个属性中:

fd表示套接字描述符,伪客户端的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接;普通客户端的fd属性的值为大于-1的整数

命令和命令参数是对输入缓冲的命令进行解析以后获得命令和参数。

cmd是命令的实现函数的数组,命令实现函数的结构如下:

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; /* Flags as string representation, one char per flag. */
// 实际flag
int flags; /* The actual flags, obtained from the 'sflags' field. */

...

// 指定哪些参数是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 */
// 统计信息
long long microseconds, calls;
};

客户端的创建和关闭

当客户端向服务器发出connect请求的时候,服务器的事件处理器就会对这个事件进行处理,创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾

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
/*
* 创建一个新客户端
*/
redisClient *createClient(int fd) {

// 分配空间
redisClient *c = zmalloc(sizeof(redisClient));

// 当 fd 不为 -1 时,创建带网络连接的客户端
// 如果 fd 为 -1 ,那么创建无网络连接的伪客户端
// 因为 Redis 的命令必须在客户端的上下文中使用,所以在执行 Lua 环境中的命令时
// 需要用到这种伪终端
if (fd != -1) {
// 非阻塞
anetNonBlock(NULL,fd);
// 禁用 Nagle 算法
anetEnableTcpNoDelay(NULL,fd);
// 设置 keep alive
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
// 绑定读事件到事件 loop (开始接收命令请求)
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}

// 初始化各个属性

// 默认数据库
selectDb(c,0);
// 套接字
c->fd = fd;
...


listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
// 如果不是伪客户端,那么添加到服务器的客户端链表中
if (fd != -1) listAddNodeTail(server.clients,c);
// 初始化客户端的事务状态
initClientMultiState(c);

// 返回客户端
return c;
}

对于客户端的启动程序,其大致的逻辑是:读取本地配置,连接服务器获取服务器的配置,获取本地输入的命令并发送到服务器

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

  • 如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
  • 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
  • 如果客户端成为了CLIENT KLLL命令的目标,那么它也会被关闭。

关闭客户端的底层实现:

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
/*
* 释放客户端
*/
void freeClient(redisClient *c) {
listNode *ln;

...

/* Free the query buffer */
sdsfree(c->querybuf);
c->querybuf = NULL;

/* Deallocate structures used to block on blocking ops. */
if (c->flags & REDIS_BLOCKED) unblockClient(c);
dictRelease(c->bpop.keys);

/* UNWATCH all the keys */
// 清空 WATCH 信息
unwatchAllKeys(c);
listRelease(c->watched_keys);

/* Unsubscribe from all the pubsub channels */
// 退订所有频道和模式
pubsubUnsubscribeAllChannels(c,0);
pubsubUnsubscribeAllPatterns(c,0);
dictRelease(c->pubsub_channels);
listRelease(c->pubsub_patterns);

/* Close socket, unregister events, and remove list of replies and
* accumulated arguments. */
// 关闭套接字,并从事件处理器中删除该套接字的事件
if (c->fd != -1) {
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
close(c->fd);
}

// 清空回复缓冲区
listRelease(c->reply);

// 清空命令参数
freeClientArgv(c);

/* Remove from the list of clients */
// 从服务器的客户端链表中删除自身
if (c->fd != -1) {
ln = listSearchKey(server.clients,c);
redisAssert(ln != NULL);
listDelNode(server.clients,ln);
}

// 删除客户端的阻塞信息
if (c->flags & REDIS_UNBLOCKED) {
ln = listSearchKey(server.unblocked_clients,c);
redisAssert(ln != NULL);
listDelNode(server.unblocked_clients,ln);
}

...

if (c->name) decrRefCount(c->name);
// 清除参数空间
zfree(c->argv);
// 清除事务状态信息
freeClientMultiState(c);
sdsfree(c->peerid);
// 释放客户端 redisClient 结构本身
zfree(c);
}

服务器

请求命令执行的过程

从客户端输入一条指令到服务端完成命令的内容并返回要经历以下这些步骤:

  1. 发送命令请求,Redis服务器的命令请求来自 Redis客户端,当用户在客户端中键人一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过套接字发送给服务器
  2. 读取命令的内容,服务器接受到套接字以后会产生一个文件事件,通过对文件事件的处理,判断为命令内容
  3. 查找命令实现,根据客户端的命令参数argv[0],在服务器的命令表中查找指定的命令,并将找到的命令保存到客户端状态的cmd属性里面
  4. 执行预备操作,在执行命令前需要进行一些操作:检查给出的命令是否有效(cmd是否为NULL);判断给定的参数是否正确;判断客户端是否通过验证
  5. 调用命令的实现函数
  6. 执行后续的工作,包括添加日志,计算时间属性,进行AOF操作等等
  7. 将命令回复发送给客户端
  8. 客户端收到并打印命令

serverCron函数

Redis服务器中的 serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转

因为serverCron的实现代码太过冗长,所以这里就简单说一些serverCron函数都干了哪些事情

  1. 更新服务器时间缓存

Redis服务器中许多的操作都需要用到当前的系统时间属性unixtime,serverCron会更新这个时间属性

  1. 更新LRU时钟

Reids服务器中实现过期键的删除需要计算其空转时间,计算空转时间需要用LRU时钟,serverCron会更新这个时钟保证Redis过期键删除功能的正常使用

  1. 更新服务器内存峰值记录

Redis中使用了一个属性stat_peak_memory记录了使用内存的峰值,这个属性需要serverCron进行更新

  1. 处理SIGTERM信号

在启动服务器时, Redis会为服务器进程的 SIGTERM信号关联处理器 sigtermhandier函数,这个信号处理器负责在服务器接到 SIGTERM信号时,打开服务器状态的 shutdown_asap标识。