You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
19 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 01 | 带你快速攻略Redis源码的整体架构
你好我是蒋德钧。从今天这节课开始我们将开启“Redis代码之旅”一起来掌握Redis的核心设计思想。
不过在正式开始我们的旅程之前还需要先做个“攻略”也就是要了解和掌握Redis代码的整体架构。
这是因为一旦掌握了Redis代码的整体架构就相当于给Redis代码画了张全景图。有了这张图我们再去学习Redis不同功能模块的设计与实现时就可以从图上快速查找和定位这些功能模块对应的代码文件。而且有了代码的全景图之后我们还可以对Redis各方面的功能特性有个全面了解这样也便于更加全面地掌握Redis的功能而不会遗漏某一特性。
那么我们究竟该如何学习Redis的代码架构呢我的建议是要掌握以下两方面内容
* **代码的目录结构和作用划分**目的是理解Redis代码的整体架构以及所包含的代码功能类别
* **系统功能模块与对应代码文件**目的是了解Redis实例提供的各项功能及其相应的实现文件以便后续深入学习。
实际上,当你掌握了以上两方面的内容之后,即使你要去了解和学习其他软件系统的代码架构,你都可以按照“先面后点”的方法来推进。也就是说,先了解目录结构与作用类别,再对应功能模块与实现文件,这样可以帮助你快速地掌握一个软件系统的代码全景。
所以在后续的学习过程中你要仔细跟住我的脚步并且手边最好能备着一台可以方便查看源码的电脑针对我提到的源码文件、关键模块或是代码运行一定要实际阅读一遍或是实操一遍这样你就能对Redis的代码架构建立更深刻的认识。
好了话不多说下面我们就一起来完成Redis代码之旅的攻略吧。
## Redis目录结构
首先我们来了解下Redis的目录结构。
为什么要从目录结构开始了解呢?其实,这是我自己**阅读代码的一个小诀窍**:在学习一个大型系统软件的代码时,要想快速地对代码有个初步认知,了解系统源码的整体目录结构就是一个行之有效的方法。这是因为,系统开发者通常会把完成同一或相近功能的代码文件,按目录结构来组织。能划归到同一个目录下的代码文件,一般都是具有相近功能目标的。
所以,从代码的目录结构开始学习,可以让我们从目录命名和目录层次结构中,直接了解到一个系统的主要组成部分。
那么对于Redis来说在它的源码总目录下一共包含了[deps](https://github.com/redis/redis/tree/5.0/deps)、[src](https://github.com/redis/redis/tree/5.0/src)、[tests](https://github.com/redis/redis/tree/5.0/tests)、[utils](https://github.com/redis/redis/tree/5.0/utils)四个子目录这四个子目录分别对应了Redis中发挥不同作用的代码下面我们具体来看看。
### deps目录
这个目录主要**包含了Redis依赖的第三方代码库**包括Redis的C语言版本客户端代码hiredis、jemalloc内存分配器代码、readline功能的替代代码linenoise以及lua脚本代码。
这部分代码的一个显著特点,就是**它们可以独立于Redis src目录下的功能源码进行编译**也就是说它们可以独立于Redis存在和发展。下面这张图显示了deps目录下的子目录内容。
![](https://static001.geekbang.org/resource/image/42/c7/4278463fb96f165bf41d6a97ff3216c7.jpg?wh=1945x726)
那么为什么在Redis源码结构中会有第三方代码库目录呢其实主要有两方面的原因。
**一方面**Redis作为一个用C语言写的用户态程序它的不少功能是依赖于标准的glibc库提供的比如内存分配、行读写readline、文件读写、子进程/线程创建等。但是glibc库提供的某些功能实现效率并不高。
我举个简单的例子glibc库中实现的内存分配器的性能就不是很高它的内存碎片化情况也比较严重。因此为了避免对系统性能产生影响Redis使用了jemalloc库替换了glibc库的内存分配器。可是jemalloc库本身又不属于Redis系统自身的功能把它和Redis功能源码放在一个目录下并不合适所以Redis使用了专门的deps目录来保存这部分代码。
**另一方面**有些功能是Redis运行所需要的但是这部分功能又会独立于Redis进行开发和演进。这种类型最为典型的功能代码就是Redis的客户端代码。
Redis作为Client-Server架构的系统访问Redis离不开客户端的支撑。此外Redis自身功能中的命令行redis-cli、基准测试程序redis-benchmark以及哨兵都需要用到客户端来访问Redis实例。
不过你应该也清楚针对客户端的开发只要保证客户端和实例交互的过程满足RESP协议就行客户端和实例的功能可以各自迭代演进。所以在Redis源码结构中C语言版本的客户端hiredis就被放到了deps目录中以便开发人员自行开发和改进客户端功能。
好了总而言之对于deps目录来说你只需要记住它主要存放了三类代码一是Redis依赖的、实现更加高效的功能库如内存分配二是独立于Redis开发演进的代码如客户端三是lua脚本代码。后续你在学习这些功能的设计实现时就可以在deps目录找到它们。
### src目录
这个目录里面**包含了Redis所有功能模块的代码文件也是Redis源码的重要组成部分**。同样我们先来看下src目录下的子目录结构。
![](https://static001.geekbang.org/resource/image/d7/26/d7ac6b01af49047409db5d9e16b6e826.jpg?wh=2187x487)
我们会发现src目录下只有一个modules子目录其中包含了一个实现Redis module的示例代码。剩余的源码文件都是在src目录下没有再分下一级子目录。
因为Redis的功能模块实现是典型的C语言风格不同功能模块之间不再设置目录分隔而是通过头文件包含来相互调用。这样的代码风格在基于C语言开发的系统软件中也比较常见比如Memcached的源码文件也是在同一级目录下。
所以当你使用C语言来开发软件系统时就可以参考Redis的功能源码结构用一个扁平的目录组织所有的源码文件这样模块相互间的引用也会很方便。
### tests目录
在软件产品的开发过程中除了第三方依赖库和功能模块源码以外我们通常还需要在系统源码中添加用于功能模块测试和单元测试的代码。而在Redis的代码目录中就将这部分代码用一个tests目录统一管理了起来。
Redis实现的测试代码可以分成四部分分别是**单元测试**对应unit子目录**Redis Cluster功能测试**对应cluster子目录、**哨兵功能测试**对应sentinel子目录、**主从复制功能测试**对应integration子目录。这些子目录中的测试代码使用了Tcl语言通用的脚本语言进行编写主要目的就是方便进行测试。
另外每一部分的测试都是一个测试集合覆盖了相应功能模块中的多项子功能测试。比如在单元测试的目录中我们可以看到有针对过期key的测试expire.tcl、惰性删除的测试lazyfree.tcl以及不同数据类型操作的测试type子目录等。而在Redis Cluster功能测试的目录中我们可以看到有针对故障切换的测试failover.tcl、副本迁移的测试replica-migration.tcl等。
不过在tests目录中除了有针对特定功能模块的测试代码外还有一些代码是**用来支撑测试功能**的这些代码在assets、helpers、modules、support四个目录中。这里我画了这张图展示了tests目录下的代码结构和层次你可以参考下。
![](https://static001.geekbang.org/resource/image/cc/5e/ccb2feae193e4911cc68a0ccb755ac5e.jpg?wh=2250x1111)
### utils目录
在Redis开发过程中还有一些功能属于辅助性功能包括用于创建Redis Cluster的脚本、用于测试LRU算法效果的程序以及可视化rehash过程的程序。在Redis代码结构中这些功能代码都被归类到了utils目录中统一管理。下图展示了utils目录下的主要子目录你可以看下。
![](https://static001.geekbang.org/resource/image/3b/b2/3b7933e5f1740ccdc3870ee554faf4b2.jpg?wh=2250x1039)
所以当我们在开发系统时就可以学习Redis的代码结构也把和系统相关的辅助性功能划归到utils目录中统一管理。
除了deps、src、tests、utils四个子目录以外Redis源码总目录下其实还包含了两个重要的配置文件一个是**Redis实例的配置文件redis.conf**,另一个是**哨兵的配置文件sentinel.conf**。当你需要查找或修改Redis实例或哨兵的配置时就可以直接定位到源码总目录下。
最后呢你也可以再次整体回顾下Redis源码的总体结构层次如下图所示。
![](https://static001.geekbang.org/resource/image/59/35/5975c57d9ac404fe3a774ea28a7ac935.jpg?wh=2238x811)
在了解了Redis的代码目录和层次以后接下来我们还需要重点学习下功能模块的源码文件即src目录下的文件内容这有助于我们在后续课程中学习Redis的相关设计思想时能够快速找到对应的源码文件。
## Redis功能模块与源码对应
Redis代码结构中的src目录包含了实现功能模块的123个代码文件。在这123个代码文件中对于某个功能来说一般包括了实现该功能的 **C语言文件.c文件** 和对应的**头文件(.h文件**。比如dict.c和dict.h就是用于实现哈希表的C文件和头文件。
> 注意在课程中如果没有特殊说明我介绍的源码都是基于Redis 5.0.8版本的。
那么我们该如何将这123个文件和Redis的主要功能对应上呢
其实,**Redis代码文件的命名非常规范文件名中就体现了该文件实现的主要功能。**比如对于rdb.h和rdb.c这两个代码文件来说从文件名上你就可以看出来它们是实现内存快照RDB的对应代码。
所以这里为了让你能快速定位源码我分别按照Redis的服务器实例、数据库操作、可靠性和可扩展性保证、辅助功能四个维度把Redis功能源码梳理成了四条代码路径。你可以根据自己想要了解的功能维度对应地学习相关代码。
### 服务器实例
首先我们知道Redis在运行时是一个网络服务器实例因此相应地就需要有代码实现服务器实例的初始化和主体控制流程而这是由server.h/server.c实现的Redis整个代码的main入口函数也是在server.c中。**如果你想了解Redis是如何开始运行的那么就可以从server.c的main函数开始看起。**
当然对于一个网络服务器来说它还需要提供网络通信功能。Redis使用了**基于事件驱动机制的网络通信框架**涉及的代码文件包括ae.h/ae.cae\_epoll.cae\_evport.cae\_kqueue.cae\_select.c。关于事件驱动框架的具体设计思路与实现方法我会在第10讲中给你详细介绍。
而除了事件驱动网络框架以外,与网络通信相关的功能还包括**底层TCP网络通信**和**客户端实现**。
Redis对TCP网络通信的Socket连接、设置等操作进行了封装这些封装后的函数实现在anet.h/anet.c中。这些函数在Redis Cluster创建和主从复制的过程中会被调用并用于建立TCP连接。
除此之外客户端在Redis的运行过程中也会被广泛使用比如实例返回读取的数据、主从复制时在主从库间传输数据、Redis Cluster的切片实例通信等都会用到客户端。Redis将客户端的创建、消息回复等功能实现在了networking.c文件中如果你想了解客户端的设计与实现可以重点看下这个代码文件。
这里我也给你总结了与服务器实例相关的功能模块及对应的代码文件,你可以看下。
![](https://static001.geekbang.org/resource/image/51/df/514e63ce6947d382fe7a3152c1c989df.jpg?wh=2250x882)
那么在了解了Redis服务器实例的主要功能代码之后我们再从Redis内存数据库这一特性维度来梳理下与它相关的代码文件。
### 数据库数据类型与操作
Redis数据库提供了丰富的键值对类型其中包括了String、List、Hash、Set和Sorted Set这五种基本键值类型。此外Redis还支持位图、HyperLogLog、Geo等扩展数据类型。
而为了支持这些数据类型Redis就使用了多种数据结构来作为这些类型的底层结构。比如String类型的底层数据结构是SDS而Hash类型的底层数据结构包括哈希表和压缩列表。
不过因为Redis实现的底层数据结构非常多所以这里我把这些底层结构和它们对应的键值对类型以及相应的代码文件列在了下表中你可以用这张表来快速定位代码文件。
![](https://static001.geekbang.org/resource/image/0b/57/0be4769a748a22dae5760220d9c05057.jpg?wh=2000x1125)
除了实现了诸多的数据类型以外Redis作为数据库还实现了对键值对的新增、查询、修改和删除等操作接口这部分功能是在**db.c文件**实现的。
当然Redis作为内存数据库其保存的数据量受限于内存大小。因此内存的高效使用对于Redis来说就非常重要。
那么你可能就要问了:**Redis是如何优化内存使用的呢**
实际上Redis是从三个方面来优化内存使用的分别是内存分配、内存回收以及数据替换。
首先,在**内存分配**方面Redis支持使用不同的内存分配器包括glibc库提供的默认分配器tcmalloc、第三方库提供的jemalloc。Redis把对内存分配器的封装实现在了zmalloc.h/zmalloc.c。
其次,在**内存回收**上Redis支持设置过期key并针对过期key可以使用不同删除策略这部分代码实现在expire.c文件中。同时为了避免大量key删除回收内存会对系统性能产生影响Redis在lazyfree.c中实现了异步删除的功能所以这样我们就可以使用后台IO线程来完成删除以避免对Redis主线程的影响。
最后,针对**数据替换**如果内存满了Redis还会按照一定规则清除不需要的数据这也是Redis可以作为缓存使用的原因。Redis实现的[数据替换策略](https://time.geekbang.org/column/article/294640)有很多种包括LRU、LFU等经典算法。这部分的代码实现在了evict.c中。
同样这里我也把和Redis数据库数据类型与操作相关的功能模块及代码文件总结成了一张图你可以看下。
![](https://static001.geekbang.org/resource/image/15/f0/158fa224d6a49c7d4702ce3f07dbeff0.jpg?wh=1938x768)
### 高可靠性和高可扩展性
首先虽然Redis一般是作为内存数据库来使用的但是它也提供了可靠性保证这主要体现在Redis可以对数据做持久化保存并且它还实现了主从复制机制从而可以提供故障恢复的功能。
这部分的代码实现比较集中,主要包括以下两个部分。
* **数据持久化实现**
Redis的数据持久化实现有两种方式**内存快照RDB** 和 **AOF日志**,分别实现在了 **rdb.h/rdb.c****aof.c** 中。
注意在使用RDB或AOF对数据库进行恢复时RDB和AOF文件可能会因为Redis实例所在服务器宕机而未能完整保存进而会影响到数据库恢复。因此针对这一问题Redis还实现了**对这两类文件的检查功能**对应的代码文件分别是redis-check-rdb.c和redis-check-aof.c。
* **主从复制功能实现**
Redis把主从复制功能实现在了**replication.c文件**中。另外你还需要知道的是Redis的主从集群在进行恢复时主要是依赖于哨兵机制而这部分功能则直接实现在了sentinel.c文件中。
其次与Redis实现高可靠性保证的功能类似Redis高可扩展性保证的功能是通过**Redis Cluster**来实现的,这部分代码也非常集中,就是在**cluster.h/cluster.c代码文件**中。所以这样我们在学习Redis Cluster的设计与实现时就会非常方便不用在不同的文件之间来回跳转了。
### 辅助功能
Redis还实现了一些用于支持系统运维的辅助功能。比如为了便于运维人员查看分析不同操作的延迟产生来源Redis在latency.h/latency.c中实现了操作延迟监控的功能为了便于运维人员查找运行过慢的操作命令Redis在slowlog.h/slowlog.c中实现了慢命令的记录功能等等。
此外运维人员有时还需要了解Redis的性能表现为了支持这一目标Redis实现了对系统进行性能评测的功能这部分代码在redis-benchmark.c中。如果你想要了解如何对Redis开展性能测试这个代码文件也值得一读。
## 小结
今天是我们了解Redis源码架构和设计思想的“热身课”这里我们需要先明确一点就是理解代码结构可以为我们提供Redis功能模块的全景图并方便我们快速查找和定位某个具体功能模块的实现源码这样也有助于提升代码阅读的效率。
我在一开始,先给你介绍了一个**小诀窍**通过目录命名和层次来快速掌握一个系统软件的代码结构。而通过学习Redis的目录结构我们也学到了一个**重要的编程规范**:在开发系统软件时,使用不同的目录对代码进行划分。
常见的目录包括保存第三方库的deps目录、保存测试用例的tests目录以及辅助功能和工具的常用目录utils目录。按照这个规范来组织你的代码就可以提升代码的可读性和可维护性。
另外在学习Redis功能模块的代码结构时面对123个代码文件我也给你分享了一种我一直比较推崇的方法**分门别类**。也就是说,按照一定的维度将所要学习的内容进行分类描述或总结。
在课程中我是按照服务器实例、数据库数据类型与操作、高可靠与高可扩展保证以及辅助功能四个维度给你梳理了四条代码路径。这四条代码路径也基本涵盖了Redis的主要功能代码可以方便你去有逻辑、有章法地学习掌握Redis源码不至于遗漏重要代码。
那么在最后我还想说一点就是在你学习了Redis源码结构的同时也希望你能把这个方法应用到其他的代码学习中提高学习效率。
## 每课一问
Redis从4.0版本开始能够支持后台异步执行任务比如异步删除数据你能在Redis功能源码中找到实现后台任务的代码文件么
欢迎在留言区分享你的思考和操作过程,我们一起交流讨论。如果觉得有收获的话,也欢迎你把今天的内容分享给更多的朋友。