当 Redis 启动时
Redis 作为现在最受欢迎的的键值数据库,在我们后端工程师的系统开发中,扮演着非常重要的角色。它超高的读写性能、丰富的存储结构、高可用集群设计以及活跃的社区,无疑是开源内存数据库的首选。我们今天就来了解一下 Redis 是如何处理一条命令的?它在代码中又用了哪些小心思来提升性能?
数据来源 db-engines.com
在开始之前
在开始之前,确保我们已经对 Redis 的安装、运行、常用命令有了一定的了解,这样我们在尝试理解内部实现原理时才会有感性的认知。我们来复习一下吧:
通过 Docker 运行一个 Redis 容器并进入
$ docker pull redis:4
$ docker exec -p 6379:6379 -itd --name redis redis:4
# 进入容器
$ docker exec -it redis sh
复制代码
通过
redis-cli
连接,执行一些命令
$ redis-cli
redis-cli(6379)> set name foo
OK
redis-cli(6379)> get name
"foo"
复制代码
我们看一下,在上面的过程中 Redis 到底发生什么?从外部视角,即客户端的视角来看,可以分解为以下几步
Redis 服务启动起来
客户端与 Redis 建立连接
客户端发送
set name foo
命令,返回响应OK
客户端发送
get name
命令,返回响应foo
那么接下来,我们从 Redis 内部视角(基于 Redis 5.0 的源码进行分析)出发,来看看,当一条命令set name foo
被 Redis 接收到时,它到底“一个人抗下了多少”?
当 Redis 启动时
当 Redis 启动时,首先会对服务器进行初始化流程,包含以下的步骤:
初始化配置(系统默认配置)
加载并解析配置文件(用户配置)
初始化服务内部变量
创建事件循环 eventLoop
创建 socket 并开始监听
创建文件事件与时间事件
开始事件循环
初始化配置
第一步操作的核心函数为initServerConfig(void)
,有两百多行,主要是对服务器的一些核心参数设置系统默认配置,比如:
核心参数 | 默认值 |
---|---|
定时任务执行频率默认 | 10 |
监听端口号 | 6379 |
最大客户端数量 | 10000 |
客户端超时时间 | 0(永不超时) |
数据库数目 | 16 |
此外,还会进行初始化常用命令的列表。 |
加载并解析配置文件
第二步操作的核心函数为void loadServerConfig(char *filename, char *options)
,参数中filename
为配置文件的路径,options
为命令行通过参数指定的配置信息,比如:
$ redis-server /config/redis/redis.conf -p 400
复制代码
则/config/redis/redis.conf
就会作为filename
,-p 400
作为options
。在解析配置文件时,会将整个配置文件内容加载到内存,通过\n
进行分割,然后将对#
开头的行进行跳过,它代表是注释。
初始化服务内部变量
第三步操作的核心函数是initServer(void)
,主要是初始化一些客户端链表、数据库、全局变量和共享变量等。Redis 通过复用共享变量来减少频繁的内存分配,通过函数createSharedObjects(void)
来实现,共享变量都被保存在全局结构体shared
中,主要有:
用于响应的字符串,比如:
shared.ok
、shared.err
0~10000的整数,比如:
shared.integers[1]
创建事件循环 eventLoop
第四步操作的核心函数是aeCreateEventLoop(int setsiee)
,创建事件循环eventLoop,即分配结构体所需内存,并初始化结构体各字段,并调用aeApiCreate
函数初始化了 epoll 对应的结构体。
创建 socket 并开始监听
第五步操作的核心函数是listenToPort(int port, int *fds, int *count)
这个函数中使用了server
变量,这是一个全局变量
// server.c
/* Global vars */
struct redisServer server; /* Server global state */
复制代码
这个变量的初始化正提到的步骤三里操作的。server.bindaddr
存储了用户在配置文件中写的所有IP地址。第五步所做的就是遍历用户配置的IP地址,建立非阻塞
的 socket,进行监听。
创建文件事件与时间事件
接下来是第六步操作是创建文件事件与时间事件,Redis 把 socket 读写事件抽象为了文件事件,即aeFileEvent
,通过事件循环进行执行。因此需要对刚才监听的 socket 创建对应的文件事件。创建文件事件的处理核心函数是
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
复制代码
我理解的是,利用 epoll 的非阻塞特性,为每个事件绑定一个对应的处理函数,当事件发生时,调用对应的处理函数进行处理即可。比如:监听事件的处理函数为acceptTcpHandle
r,实现了 socket 连接请求的 accept ,以及客户端对象的创建。
时间事件由定时任务函数触发,核心处理函数是aeCreateTimeEvent
,时间事件实际上只有一个,他通过链表连接多个定时任务。
开始事件循环
最后一步是开始事件循环,核心处理函数代码很少,我们来看一下
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
复制代码
注意到eventLoop->beforesleep
函数,在每次事件循环前执行,它会执行一些不是很费时的操作,如:集群相关操作、过期键删除操作(这里可称为快速过期键删除)、向客户端返回命令回复等。之后就是aeProcessEvents
函数的执行,它会阻塞等待一小会儿文件事件的响应,如果有结果了,就去执行对应的处理函数,然后再去执行一下时间事件。
总结
本篇讲解了当 Redis 启动时程序做了哪些动作:初始化配置和变量、创建事件循环、监听 socket、创建文件事件和时间事件等等。也了解到 Redis 的全局变量和共享变量的作用,在共享变量中初始化0~10000的整数这种操作,和之前学习 Python 内存管理时它的小整数池是一个思路(空间换是时间)。
作者:Zioyi
链接:https://juejin.cn/post/7027808574306779166
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。