Redis 源码研究(零) - 源码阅读指南(The Beginning)
Redis 于 readme
提供了源码阅读指南,本文对其进行了简单的翻译,英语水平有限以及部分错别字还请谅解。
欢迎转载,请注明出处,谢谢!
Redis Internals
如果你正在读这个 readme
的话,你应该正在 github 浏览或者刚刚解压了压缩包,无论如何,你距离源码仅有一步之遥了。
所以在此我们介绍一下 Redis 的源码结构,每个文件的大体思想、Redis 服务器中最重要的函数和结构等等。
我们尽量在高层次进行讨论而避免深入细节,否则这份文档就将太长了,而且我们的代码也会持续进行变动,但从大体思想开始理解总是一个好的选择。此外,大部分的代码都有详细的注释便于阅读。
源码结构
Redis 的根目录仅包含了 README、调用 src
文件夹内 Makefile 的 Makefile 以及一份 Redis 和 Sentinel 的配置文件例子。你可以找到几个执行各个单元测试的脚本,它们都在 tests
文件夹下实现。
根目录下有以下重要的子文件夹:
src
: 包含 Redis 实现,以 C 语言编写tests
: 包含单元测试,以 Tcl 实现deps
: 包含了 Redis 使用的库。所有编译 Redis 需要的库都在这个文件夹下;你的系统只需要提供libc
,一个兼容 POSIX 的接口以及一个 C 编译器即可。值得注意的是deps
包含了一份jemalloc
的副本,它是 Redis 在 Linux 下的默认内存分配器。可以看到在deps
文件夹下也有许多内容是 Redis 项目组发起的,但去具体讨论它们的话就不是antirez/redis
仓库了。
我们目前先专注于包含 Redis 具体实现的 src
文件夹,去看看每个文件中都有什么,其他几个文件夹目前对我们并不重要。下面展示的文件顺序是为了从不同层次解构复杂度而从逻辑层面安排的。
注意:最近 Redis 进行了一些重构,函数名称以及文件名称可能会有变动,所以这篇文档所反映的可能更接近于 unstable
分支的内容;例如在 Redis 3.0 中 server.c
和 server.h
被重命名为了 redis.c
和 redis.h
,然而总的来说它们的结构都是一样的。记住新的开发和 PR 都应该在 unstable
分支中提交。
server.h
理解程序如何工作的最简单方法就是去理解它使用的数据结构,所以我们首先从 Redis 主要的头文件开始看起,那就是 server.h
。
所有的服务器配置或者说通用的状态定义于一个全局结构 server
,类型为 struct redisServer
,这个结构的一些重要字段如下:
server.db
是 Redis 存储数据的数据库数组server.commands
是命令表(command table
)server.clients
是一个记录连接至服务器客户端的链表server.master
是一个特殊的客户端,指代主服务器,如果当前实例是从属服务器的话
除了这些还有很多其他字段,大部分字段都直接在结构定义处注释了。
Redis 的另一个重要的结构就是定义客户端的结构了,之前它叫 redisClient
,现在仅仅叫做 client
。这歌结构体也有许多字段,我们仅仅看一些主要的字段:
struct client {
int fd;
sds querybuf;
int argc;
robj **argv;
redisDb *db;
int flags;
list *reply;
char buf[PROTO_REPLY_CHUNK_BYTES];
... many other fields ...
}
客户端结构体定义了一个 处于连接状态的客户端:
fd
是客户端的 socket 描述符argc
和argv
根据客户端正在执行的指令不同而不同,使得实现指定命令的函数能够读取这些参数querybuf
是客户端请求的累计缓冲区,会被 Redis 服务器按照 Redis 协议解析并根据客户端执行指令而执行实际的指令实现。reply
和buf
分别是服务器发送给客户端数据的动态及静态缓冲区,这些缓冲区中的数据会在套接字可写时尽早地写入
根据上面提到的结构,一个命令的参数由 robj
结构所描述,下面是 robj
的完整定义,它定义了一个 Redis 对象:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
基本这个结构可以代表 Redis 的所有数据类型了,例如字符串、列表、集合、有序集合等,值得注意的是,利用 type
字段可以知道一个给定的对象是什么类型,而利用 refcount
字段可以使得一个相同的对象可以在多处被引用而不必多次分配内存,而 ptr
字段指向了对象的实际表示,取决于所使用的 encoding
(编码)甚至对于相同的类型其实际表示也可能不同。
Redis 对象在 Redis 内部使用非常广泛,然而为了避免过度的间接访问,最近在许多地方我们使用朴素的动态字符串而不是利用 Redis 对象包装。
server.c
这是定义了 main()
函数的 Redis 服务器的入口,为了启动 Redis 服务器,下面是一些重要的步骤:
initServerConfig()
对server
结构进行初始化initServer()
分配需要操作的数据结构,启动监听套接字等等aeMain()
开启监听新连接的事件循环
有两个在事件循环中周期性调用的特殊函数:
serverCron()
根据server.hz
的频率周期性地调用,执行那些必须按时完成的一些任务比如检查超时的客户端beforeSleep()
在每次事件循环被击发时调用,让 Redis 先处理一些请求然后再返回事件循环
在 server.c
里面你可以找到处理其他一些重要事情的代码:
call()
被用来在一个给定的客户端上下文执行指定的命令activeExpireCycle()
回收使用了EXPIRE
命令设置的具有生命周期的一些 keyfreeMemoryIfNeeded()
在一个新的写命令即将被执行但 Redis 所使用的内存超出了maxmemory
指令的限制的时候被调用- 全局变量
redisCommandTable
定义了所有的 Redis 指令,指定了命令的名字、实现命令的函数、命令所需的参数数量以及每个命令的其他属性
networking.c
这个文件定义了所有与客户端的 I/O 函数以及主从操作(在 Redis 里它们仅仅是特殊的客户端而已)
createClient()
分配并初始化一个新的客户端addReply*()
的一系列函数是各个命令实现用于向客户端结构追加数据的,那会作为指令执行的结果发送给客户端writeToClient()
将输出缓冲区的数据传输给客户端,这个函数被 可写事件处理函数sendReplyToClient()
调用readQueryFromClient()
是 可读事件处理函数,它将从客户端读取到的数据写入查询缓冲区processInputBuffer()
是解析客户端查询缓冲区数据的入口,一旦指令要被处理了它就会调用在server.c
中定义的processCommand()
来执行命令freeClient()
释放、断开客户端的连接并移除客户端
aof.c 以及 rdb.c
从文件名就可以猜出这两个文件是用于实现 Redis 的 RDB 和 AOF 持久化的;Redis 使用基于系统调用 fork()
的持久化模型以便创建拥有同主线程共享内存的线程,这个线程会将内存中的数据存储至磁盘。rdb.c
实现了创建快照而 aof.c
实现了当(Append Only)仅追加文件过大时执行 AOF rewrite。
在 aof.c
内部的实现中有一些为了让客户端执行命令时能够将命令写入 AOF 文件的接口,而实现的额外的函数
server.c
中定义的 call()
函数负责调用这个函数来将命令写入 AOF 中。
db.c
一些 Redis 指令操作固定的数据类型,也有一些通用性指令例如 DEL
和 EXPIRE
,它们基于键执行操作而不依赖于它们实际的值,所有的这些通用性指令都定义在 db.c
中。
此外 db.c
还实现了一个用于在 Redis 数据集上执行指定操作而无需访问其内部结构的的接口。
在 db.c
中被许多指令的实现都是用到的最重要的一些函数如下:
lookupKeyRead()
和lookupKeyWrite()
会根据给定的 key 查询指向 value 的指针,如果 key 不存在的话则返回NULL
dbAdd()
以及其更高层版本setKey()
在 Redis 数据库中创建一个新的 keydbDelete()
删除一个 key 及其对应的 valueemptyDb()
删除一整个数据库或者删除所有定义的数据库
文件中剩下的内容实现了暴露给客户端的通用型指令
object.c
之前提到了 robj
结构定义了 Redis 对象,在 object.c
中提供了底层操作 Redis 对象的方法,例如分配新对象、处理引用计数的函数等,在这个文件中值得注意的函数有:
incrRefcount()
和decrRefCount()
用于增加或减少一个对象的引用计数,当引用计数为 0 时对象被释放createObject()
分配一个新对象,也有一些例如用于分配指定内容的字符串对象的一些特化函数createStringObjectFromLongLong()
等。
这个文件也实现了 OBJECT
指令。
replication.c
这是 Redis 中最复杂的文件之一了,建议对其他代码基础有一定的熟悉之后再来钻研这部分,在这个文件中实现了 Redis 的主从角色关系。
其中最重要的函数之一就是(向连接主数据库的)代表了从属数据库的客户端写指令的 replicationFeedSlaves()
,使得从属数据库可以获得客户端发送的数据:使用这种方法时从属数据库会与主数据库保持同步。
这个文件也实现了用于执行首次主从间数据同步或在主从连接断开后的恢复 SYNC
和 PSYNC
指令。
其他的 C 文件
t_hash.c
,t_list.c
,t_test.c
,t_string.c
,t_zset.c
和t_stream.c
包含了 Redis 数据类型的实现,它们不仅实现了对给定数据类型的 API 访问,也实现了在这些数据类型上的客户端指令ae.c
实现了 Redis 的事件循环,是一个容易阅读和理解的相对独立的库sds.c
是 Redis 的字符串库,可以在 https://github.com/antirez/sds 看到更多信息anet.c
是一个与内核接口相比更易用的 POSIX 网络库dict.c
是一个增量再哈希(rehash)的非阻塞哈希表的实现scripting.c
实现了 Lua 脚本,它和 Redis 的其他部分完全独立而且也足够容易理解(如果你熟悉 Lua API 的话)cluster.c
实现了 Redis 集群,可能在对 Redis 其他部分比较熟悉之后再阅读比较好,如果你想看cluster.c
的话,确保阅读 Redis 集群技术手册
Redis 指令剖析
所有的 Redis 指令都以下面的方式定义:
void foobarCommand(client *c) {
printf("%s",c->argv[1]->ptr); /* Do something with the argument. */
addReply(c,shared.ok); /* Reply something to the client. */
}
这个指令在 server.c
的指令表中以如下的方式引用:
{"foobar",foobarCommand,2,"rtF",0,NULL,0,0,0,0,0},
在上面的例子中 2
是指令所需的参数个数,"rtF"
是指令的标识(flag),可以在 server.c
的指令表顶部的注释中看到 flag 的说明。
在指令处理完成之后,它会返回一个信息给客户端,一般使用 addReply()
或者定义于 networking.c
中的其他相似函数。
在 Redis 源码中有许多指令的实现都可以作为实际指令实现的例子,多写一些测试指令是熟悉代码基础的好方法。
也有许多文件在这并没有提到,但要覆盖所有内容用处并不大,我们只是想帮你踏出第一步,最终你会在 Redis 的代码中找到一条属于你自己的路(Find your own way)。
Enjoy!