当 Redis 启动时

共 3139字,需浏览 7分钟

 ·

2021-11-09 02:46

Redis 作为现在最受欢迎的的键值数据库,在我们后端工程师的系统开发中,扮演着非常重要的角色。它超高的读写性能、丰富的存储结构、高可用集群设计以及活跃的社区,无疑是开源内存数据库的首选。我们今天就来了解一下 Redis 是如何处理一条命令的?它在代码中又用了哪些小心思来提升性能?

数据来源 db-engines.com

在开始之前

在开始之前,确保我们已经对 Redis 的安装、运行、常用命令有了一定的了解,这样我们在尝试理解内部实现原理时才会有感性的认知。我们来复习一下吧:

  1. 通过 Docker 运行一个 Redis 容器并进入

$ docker pull redis:4
$ docker exec -p 6379:6379 -itd --name redis redis:4
# 进入容器
$ docker exec -it redis sh
复制代码
  1. 通过redis-cli连接,执行一些命令

$ redis-cli
redis-cli(6379)> set name foo
OK
redis-cli(6379)> get name
"foo"
复制代码

我们看一下,在上面的过程中 Redis 到底发生什么?从外部视角,即客户端的视角来看,可以分解为以下几步

  1. Redis 服务启动起来

  2. 客户端与 Redis 建立连接

  3. 客户端发送set name foo命令,返回响应OK

  4. 客户端发送get name命令,返回响应foo

那么接下来,我们从 Redis 内部视角(基于 Redis 5.0 的源码进行分析)出发,来看看,当一条命令set name foo被 Redis 接收到时,它到底“一个人抗下了多少”?

当 Redis 启动时

当 Redis 启动时,首先会对服务器进行初始化流程,包含以下的步骤:

  1. 初始化配置(系统默认配置)

  2. 加载并解析配置文件(用户配置)

  3. 初始化服务内部变量

  4. 创建事件循环 eventLoop

  5. 创建 socket 并开始监听

  6. 创建文件事件与时间事件

  7. 开始事件循环

初始化配置

第一步操作的核心函数为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.okshared.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 的非阻塞特性,为每个事件绑定一个对应的处理函数,当事件发生时,调用对应的处理函数进行处理即可。比如:监听事件的处理函数为acceptTcpHandler,实现了 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



浏览 69
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报