你好,我是蒋德钧。从今天这节课开始,我们将开启“Redis代码之旅”,一起来掌握Redis的核心设计思想。

不过,在正式开始我们的旅程之前,还需要先做个“攻略”,也就是要了解和掌握Redis代码的整体架构。

这是因为,一旦掌握了Redis代码的整体架构,就相当于给Redis代码画了张全景图。有了这张图,我们再去学习Redis不同功能模块的设计与实现时,就可以从图上快速查找和定位这些功能模块对应的代码文件。而且,有了代码的全景图之后,我们还可以对Redis各方面的功能特性有个全面了解,这样也便于更加全面地掌握Redis的功能,而不会遗漏某一特性。

那么,我们究竟该如何学习Redis的代码架构呢?我的建议是要掌握以下两方面内容:

实际上,当你掌握了以上两方面的内容之后,即使你要去了解和学习其他软件系统的代码架构,你都可以按照“先面后点”的方法来推进。也就是说,先了解目录结构与作用类别,再对应功能模块与实现文件,这样可以帮助你快速地掌握一个软件系统的代码全景。

所以,在后续的学习过程中,你要仔细跟住我的脚步,并且手边最好能备着一台可以方便查看源码的电脑,针对我提到的源码文件、关键模块或是代码运行,一定要实际阅读一遍或是实操一遍,这样你就能对Redis的代码架构建立更深刻的认识。

好了,话不多说,下面我们就一起来完成Redis代码之旅的攻略吧。

Redis目录结构

首先,我们来了解下Redis的目录结构。

为什么要从目录结构开始了解呢?其实,这是我自己阅读代码的一个小诀窍:在学习一个大型系统软件的代码时,要想快速地对代码有个初步认知,了解系统源码的整体目录结构就是一个行之有效的方法。这是因为,系统开发者通常会把完成同一或相近功能的代码文件,按目录结构来组织。能划归到同一个目录下的代码文件,一般都是具有相近功能目标的。

所以,从代码的目录结构开始学习,可以让我们从目录命名和目录层次结构中,直接了解到一个系统的主要组成部分。

那么对于Redis来说,在它的源码总目录下,一共包含了depssrctestsutils四个子目录,这四个子目录分别对应了Redis中发挥不同作用的代码,下面我们具体来看看。

deps目录

这个目录主要包含了Redis依赖的第三方代码库,包括Redis的C语言版本客户端代码hiredis、jemalloc内存分配器代码、readline功能的替代代码linenoise,以及lua脚本代码。

这部分代码的一个显著特点,就是它们可以独立于Redis src目录下的功能源码进行编译,也就是说,它们可以独立于Redis存在和发展。下面这张图显示了deps目录下的子目录内容。

那么,为什么在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目录下的子目录结构。

我们会发现,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目录下的代码结构和层次,你可以参考下。

utils目录

在Redis开发过程中,还有一些功能属于辅助性功能,包括用于创建Redis Cluster的脚本、用于测试LRU算法效果的程序,以及可视化rehash过程的程序。在Redis代码结构中,这些功能代码都被归类到了utils目录中统一管理。下图展示了utils目录下的主要子目录,你可以看下。

所以,当我们在开发系统时,就可以学习Redis的代码结构,也把和系统相关的辅助性功能划归到utils目录中统一管理。

好,除了deps、src、tests、utils四个子目录以外,Redis源码总目录下其实还包含了两个重要的配置文件,一个是Redis实例的配置文件redis.conf,另一个是哨兵的配置文件sentinel.conf。当你需要查找或修改Redis实例或哨兵的配置时,就可以直接定位到源码总目录下。

最后呢,你也可以再次整体回顾下Redis源码的总体结构层次,如下图所示。

好,在了解了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.c,ae_epoll.c,ae_evport.c,ae_kqueue.c,ae_select.c。关于事件驱动框架的具体设计思路与实现方法,我会在第10讲中给你详细介绍。

而除了事件驱动网络框架以外,与网络通信相关的功能还包括底层TCP网络通信客户端实现

Redis对TCP网络通信的Socket连接、设置等操作进行了封装,这些封装后的函数实现在anet.h/anet.c中。这些函数在Redis Cluster创建和主从复制的过程中,会被调用并用于建立TCP连接。

除此之外,客户端在Redis的运行过程中也会被广泛使用,比如实例返回读取的数据、主从复制时在主从库间传输数据、Redis Cluster的切片实例通信等,都会用到客户端。Redis将客户端的创建、消息回复等功能,实现在了networking.c文件中,如果你想了解客户端的设计与实现,可以重点看下这个代码文件。

这里我也给你总结了与服务器实例相关的功能模块及对应的代码文件,你可以看下。

那么,在了解了Redis服务器实例的主要功能代码之后,我们再从Redis内存数据库这一特性维度,来梳理下与它相关的代码文件。

数据库数据类型与操作

Redis数据库提供了丰富的键值对类型,其中包括了String、List、Hash、Set和Sorted Set这五种基本键值类型。此外,Redis还支持位图、HyperLogLog、Geo等扩展数据类型。

而为了支持这些数据类型,Redis就使用了多种数据结构来作为这些类型的底层结构。比如,String类型的底层数据结构是SDS,而Hash类型的底层数据结构包括哈希表和压缩列表。

不过,因为Redis实现的底层数据结构非常多,所以这里我把这些底层结构和它们对应的键值对类型,以及相应的代码文件列在了下表中,你可以用这张表来快速定位代码文件。

除了实现了诸多的数据类型以外,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实现的数据替换策略有很多种,包括LRU、LFU等经典算法。这部分的代码实现在了evict.c中。

同样,这里我也把和Redis数据库数据类型与操作相关的功能模块及代码文件,总结成了一张图,你可以看下。

高可靠性和高可扩展性

首先,虽然Redis一般是作为内存数据库来使用的,但是它也提供了可靠性保证,这主要体现在Redis可以对数据做持久化保存,并且它还实现了主从复制机制,从而可以提供故障恢复的功能。

这部分的代码实现比较集中,主要包括以下两个部分。

Redis的数据持久化实现有两种方式:内存快照RDBAOF日志,分别实现在了 rdb.h/rdb.caof.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功能源码中,找到实现后台任务的代码文件么?

欢迎在留言区分享你的思考和操作过程,我们一起交流讨论。如果觉得有收获的话,也欢迎你把今天的内容分享给更多的朋友。

评论