一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期。

相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入手,结果就和博主一样,一拖再拖。

但正所谓,种一棵树的最好时间是十年前,其次就是现在。如果你真的想了解 Redis 源码,又有缘看到了这系列博文,何不跟着博主一起解读 Redis 源码,做个同行人呢?接下来,就让我们一起走入 Redis 的源码世界吧。

决定要读了,下一步就是如何读。从 github 上克隆下来源码,一看 src 目录,望天,104 个文件,我该从哪个文件开始呢?一个个文件看?不行不行,这样对我毫无诱惑力,没有诱惑力,怎么能战胜游戏、小说对我的吸引呢?苦苦思考,不得其解。然后突然想起来 HTTP 协议的那个经典面试题:从浏览器输入网址,到页面展示,这个过程发生了什么?

把这个面试题换成 Redis:输入开启 Redis 服务的命令,回车,到成功启动 Redis 服务,这个过程发生了什么?

很好,这个问题成功吸引到我了。就让我们从源码中找出这个问题的答案吧。后续的所有文章我们都尝试通过提出问题,解答问题的步骤,来深入了解 Redis。

要了解 Redis 命令的执行过程,首先要安装 Redis 服务,搭建 debug 环境。如果我们能一行行的看到命令在代码中的执行过程,解读源码也就没任何阻碍了。

后续所有文章均基于 redis3.2.13 版本。

1 搭建 debug 环境

1、下载编译文件

在 linux 上,下载源码文件,编译,使用 gdb(cgdb) 进行 debug。

# bash
wget https://github.com/antirez/redis/archive/3.2.13.tar.gz
tar -zxvf 3.2.13.tar.gz
mv redis-3.2.13 /opt/
cd redis-3.2.13
make # 编译文件,得到可执行文件 redis-server、redis-cli 等

2、开启 debug

# bash
gdb src/redis-server # 在 redis 安装目录,进入 gdb 调试环境

按我们平时调试的习惯,找到一个函数设置断点,然后一步步运行调试。对于 Redis 也一样,我们找到 server.c 文件,服务器运行的 main 函数就在此文件中。我们对 main 函数设置断点:

# gdb
(gdb) b main
Breakpoint 1 at 0x42ed05: file server.c, line 3962.

页面会提示我们在 server.c 文件的 3962 行设置了断点,也就是我们指定的 main 函数的位置。

设置好断点,下一步就是启动服务:

// 启动服务
(gdb) r ./redis.conf
Starting program: /opt/redis-3.2.13/src/redis-server ./redis.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argc=2, argv=0x7fffffffe5a8) at server.c:3962
3962 int main(int argc, char **argv) {

通过页面输出信息,我们会发现程序已经运行到我们设置的断点了。但是我们看不到运行处的代码,这可不行,看不到源码的调试,没法接受使用以下命令”召唤“源码:

(gdb) layout src

出现下图所示的界面:

到了这一步,我们已经正式开始踏上 Redis 源码解读之路了。

2 初始化服务

继续往下走,使用 n 命令,执行下一步,然后不断回车、回车、回车,好像每一行都看不懂什么意思。不管了,继续走。咦,好像发现个能看懂的 initServerConfig()。没看错的话,这个应该是初始化服务器配置的,让我们进到这个函数里确认下:

(gdb) s

回车,走你。然后我们就看到了下面这个界面:

提示我们进入了 server.c 1464 行的 initServerConfig 函数中。 n 命令,继续走。我们会发现在这个函数里对服务器的各种基础参数进行初始化。这里的参数详见 server.h/redisServer 结构体。

回到 main 函数后,我们继续前进,还会发现一个 initServer() 的函数。这个函数是进行驱动事件的注册,以及绑定回调函数等。

继续走,直到执行 aeMain(),如下图:

程序执行到 4133 行时,Redis 服务已成功开启了。此时服务器处于休眠状态,并使用 aeMain() 进行事件轮询,等待监听事件的发生。

上述整个过程,我们只是跟着程序的运行,大概看了一遍执行流程。下面,我们来详细解读上面叙述的关键步骤:初始化基础配置初始化服务器数据结构

3 初始化详细解读

3.1 初始化基础配置

初始化服务器的第一步就是创建一个 ````redisServer``` 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值。

void initServerConfig(void) {
int j;
// 设置服务器运行 ID
getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE); // 为运行 ID 加上结尾字符
server.runid[CONFIG_RUN_ID_SIZE] = '\0'; // 设置服务器默认运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32; // 设置服务器默认配置文件路径
server.configfile = NULL; // 设置服务器默认运行频率
server.hz = CONFIG_DEFAULT_HZ; // 设置服务器默认端口
server.port = CONFIG_DEFAULT_SERVER_PORT; // ...
}

对于 initServerConfig 函数来说,它主要完成以下主要工作:

  • 设置服务器的运行 ID。
  • 设置服务器的默认运行频率。
  • 设置服务器的默认配置文件路径。
  • 设置服务器的运行架构。
  • 设置服务器的默认端口号

initServerConfig 函数设置的服务器状态属性基本上都是一些整数、浮点数或者字符串属性。除了命令吧之外,initServerConfig 函数没有创建服务器状态的其它数据结构。像数据库、慢查询日志、Lua 环境、共享对象等这些数据结构是在之后的步骤中创建的。

当初始化基础配置参数后,下一步就要开始载入配置选项

3.2 载入配置选项

在启动服务器时,用户可以通过给定配置参数或者知道配置文件来修改服务器的默认配置。就像我们可以在启动服务时指定端口:

# bash
./src/redis-server --port 7379

通过给定配置参数的方式,修改了服务器的运行端口号。

除了给定配置参数的方式,我们可以通过指定配置文件的形式启动服务:

# bash
./src/redis-server ./redis.conf

通过指定配置文件的形式启动服务时,我们实际上就是通过配置文件的形式修改了服务器的数据库配置。

服务器在用 initServerConfig 函数初始完 server 变量后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对 server 变量相关属性进行修改。

关于命令行指定配置、配置文件配置、默认配置,这三种配置中:

  • 如果有指定配置,服务器就是有用户指定的值来更新对应的属性。
  • 如果没有指定值,则沿用 initServerConfig 函数设置的默认值。

3.3 初始化服务器数据结构

在执行 initServerConfig 函数初始化配置时,程序只创建了命令表一个数据结构,而服务器除了命令表还包括其他数据结构,比如:

  • server.clients 链表。这个链表记录了所有与服务器相连的客户端的状态结构。链表的每个节点都包含了一个 RedisClient 结构实例。
  • server.db 数组。数组中包含了服务器所有的数据库。
  • server.pubsub_channels 字典。字典中保存频道订阅信息。
  • server.pubsub_patterna 链表。链表中保存模式订阅信息。
  • server.lua 属性。用来执行 Lua 脚本。
  • server.slowlog 属性。用来保存慢日志。

上述这些数据结构会在 initServer 函数为其分配内存,并在有需要时为这些数据结构设置或关联初始化值。

之所以在载入用户配置之后才初始化数据结构,就是因为服务器要先载入用户的配置选项,才能根据选项正确的对数据结构进行初始化。避免再根据用户配置修改数据结构相关属性。

所以,我们可以看出,服务器对状态的初始化分为两步进行:

  1. initServerConfig 函数是初始化一般属性。
  2. initServer 初始化数据结构。

除了初始化数据结构之外,initServer 还进行了一些非常重要的设置操作,包括:

  • 为服务器设置进程信号处理器。
  • 创建共享对象。这些对象包含 Redis 服务器常用到的一些只,比如包含 "OK" 回复的字符串对象,包含 "ERR" 回复的字符串对象,包含整数 1 到 10000 的字符串对象等等。服务器正是通过重用这些共享对象来避免反复创建相同的对象,节约内存。
  • 打开服务器的监听端口,并为监听套接字关联应答事件处理器,等待服务器正式运行时接受客户端的连接。
  • 为服务器创建时间事件,等待服务器正是运行时执行 serverCron 函数。
  • 如果开启了 AOF 持久化功能,打开现有的 AOF 文件。如果 AOF 文件不存在,就创建并打开新的 AOF 文件,为 AOF 写入做好准备。
  • 初始化服务器的后台 IO 模块,为 IO 操作做好准备。

initServer 函数执行完毕之后,服务器将用 ASCII 字符在日志中打印出我们常见到的 Redis 图标,以及 Redis 的版本号信息等。

4 其它操作

4.1 还原数据库

在完成了对服务器状态 server 变量的初始化之后,服务器需要载入 RDB 文件或者 AOF 文件(数据持久化保存文件),并根据文件记录的内容来还原服务器的数据库状态。

还原过程中,服务器会判断是否启用了 AOF 持久化功能:

  • 如果启用了 AOF 持久化功能,服务器将使用 AOF 文件来还原数据库状态。
  • 如果没有启用 AOF,服务器使用 RDB 文件来还原数据库状态。

当服务器完成数据库状态还原工作之后,会在日志中打印出载入文件和还原数据库状态所耗费的时长。

8189:M 31 May 13:12:47.971 * DB loaded from disk: 0.000 seconds

4.2 执行事件循环

在初始化的最后一步,服务器将打印出以下日志:

8189:M 31 May 13:12:47.971 * The server is now ready to accept connections on port 8379

并开始执行服务器的事件循环。

至此,服务器的初始化工作全部完成。

5 gdb 基础使用

命令 解释 示例
gdb file 加载被调试的可执行程序文件 gdb src/redis-server
r Run 的缩写,运行被调试的程序。 r ./redis.conf
c Continue 的缩写。继续执行被调试程序,直至下一个断点或程序结束 c
b Breakpoint 缩写。设置断点。可以使用 行号、函数名称、执行地址等方式指定断点位置 b main
s/n s 相当于“单步跟踪并进入”,也就是说进入到执行的函数内部。n 相当于“单步跟踪”,不进入到执行函数内部 s/n
p 变量名称 Print 缩写。显示指定变量的值。 p server

总结

  1. 搭建环境三步走:下载、编译、gdb。
  2. 服务启动包括:初始化基础配置、数据结构、对外提供服务的准备工作、还原数据库、执行事件循环等。
  3. gdb 基础命令:r c b n p。

跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?的更多相关文章

  1. 跟着大彬读源码 - Redis 2 - 服务器如何响应客户端请求?(上)

    上次我们通过问题"启动服务器,程序都干了什么?",跟着源码,深入了解了 Redis 服务器的启动过程. 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情.这 ...

  2. 跟着大彬读源码 - Redis 3 - 服务器如何响应客户端请求?(下)

    继续我们上一节的讨论.服务器启动了,客户端也发送命令了.接下来,就要到服务器"表演"的时刻了. 1 服务器处理 服务器读取到命令请求后,会进行一系列的处理. 1.1 读取命令请求 ...

  3. 跟着大彬读源码 - Redis 4 - 服务器的事件驱动有什么含义?(上)

    众所周知,Redis 服务器是一个事件驱动程序.那么事件驱动对于 Redis 而言有什么含义?源码中又是如何实现事件驱动的呢?今天,我们一起来认识下 Redis 服务器的事件驱动. 对于 Redis ...

  4. 跟着大彬读源码 - Redis 6 - 对象和数据类型(下)

    继续撸我们的对象和数据类型. 上节我们一起认识了字符串和列表,接下来还有哈希.集合和有序集合. 1 哈希对象 哈希对象的可选编码分别是:ziplist 和 hashtable. 1.1 ziplist ...

  5. 跟着大彬读源码 - Redis 7 - 对象编码之简单动态字符串

    Redis 没有直接使用 C 语言传统的字符串表示(以空字符串结尾的字符数组),而是构建了一种名为简单动态字符串(simple dynamic string)的抽象类型,并将 SDS 用作 Redis ...

  6. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  7. 跟着大彬读源码 - Redis 9 - 对象编码之 三种list

    目录 1 ziplist 2 skiplist 3 quicklist 总结 Redis 底层使用了 ziplist.skiplist 和 quicklist 三种 list 结构来实现相关对象.顾名 ...

  8. 跟着大彬读源码 - Redis 10 - 对象编码之整数集合

    [TOC] 整数集合是 Redis 集合键的底层实现之一.当一个集合只包含整数值元素,并且元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现. 1 整数集合的实现 整数集合是 Redis ...

  9. 跟着大彬读源码 - Redis 5 - 对象和数据类型(上)

    相信很多人应该都知道 Redis 有五种数据类型:字符串.列表.哈希.集合和有序集合.但这五种数据类型是什么含义?Redis 的数据又是怎样存储的?今天我们一起来认识下 Redis 这五种数据结构的含 ...

随机推荐

  1. 关于ajax入门案例

    $.ajax方法 1.url: 要求为String类型的参数,(默认为当前页地址)发送请求的地址. 2.type: 要求为String类型的参数,请求方式(post或get)默认为get.注意其他ht ...

  2. WPF实现选项卡效果(2)——动态添加AvalonDock选项卡

    原文:WPF实现选项卡效果(2)--动态添加AvalonDock选项卡 简介 在前面一篇文章里面,我们使用AvalonDock实现了类似于VS的选项卡(或者浏览器的选项卡)效果.但是我们是通过xaml ...

  3. style的继承

    第一种方式:瞄准控件的基类 如下例所示,继承ContentControl的控件,都可以使用这个Style <Window.Resources> <Style x:Key=" ...

  4. WPF DataGrid 的RowDetailsTemplate的使用

    <Window x:Class="DataGridExam.DataGridRowDetailsExam"        xmlns="http://schemas ...

  5. Delphi调试activex

    以前好多次遇到了activex无法调试的问题,一直没搞清楚原因,最近终于搞清楚了,原来是IE由单线程变成了多线程. 下面就说说调试activex的方法 一.简单的方式,这种方式只适用于浏览器为单线程的 ...

  6. vs2017 js cordova + dotnet core 开发app

    原文:vs2017 js cordova + dotnet core 开发app 1.记得在index.html加入 <meta http-equiv="Content-Securit ...

  7. 不能继承于QObject的类就一定不能使用信号槽?(用一个代理类进行发射就行了)

    首先不能继承QObject的情况在开发中遇到得并不多,笔者在一年多的Qt项目开发中只遇到两三次.而且都是因为引进了第三方库导致编译过程中报错. 要想解决这个问题其实不难,因为笔者遇到的问题都是想定义一 ...

  8. 天气预报API接口

    原文:天气预报API接口 一.中央气象台API接口: 1. XML接口 http://flash.weather.com.cn/wmaps/xml/china.xml 这个是全国天气的根节点,列出所有 ...

  9. Have You Tried Delphi on Amazon Linux? (就是AWS用的Linux)

    The new Delphi Linux compiler enables customers to take new or existing Windows server applications ...

  10. GIS基础软件及操作(十二)

    原文 GIS基础软件及操作(十二) 练习十二. ArcMap制图-地图版面设计 设置地图符号-各种渲染方式的使用 使用ArcMap Layout(布局)界面制作专题地图 将各种地图元素添加到地图版面中 ...