Redis 0.09x 源码阅读入口文件
0.09x 是目前能够下载到的最古老的版本,在代码中有标注,这是一个预览版,所有代码加起来在 1W 多行,其中包括了文档,和一些辅助脚本,第三方案例,真正的 C 语言实现的代码仅有 5200 多行,这其中其实已经包含了很多 Redis 的核心逻辑,阅读起来的障碍也会小很多。
Reids 7 的C 代码行数已经超过了15W行,阅读起来的难度可想而知。
我们先来看一下 redis 的目录结构
这个时候的目录非常简洁,工程目录下直接存放了所有的源代码,以及几个目录。
- client-libraries 存放了各个语言的 client sdk,我们直接可以利用其中的 sdk 进行与 redis-server 的交互。
- doc 存放了 redis 的所有文档。
- utils 是 redis 的工具包,使用 ruby 进行编写的。
这个时候的 Makefile 异常简洁,没有后面那么复杂。一共只有 62 行,只要拥有一定的 C 语言基础相信直接就能看明白。
# Redis Makefile
# Copyright (C) 2009 Salvatore Sanfilippo <antirez at gmail dot com>
# This file is released under the BSD license, see the COPYING file
DEBUG?= -g
CFLAGS?= -std=c99 -pedantic -O2 -Wall -W -DSDS_ABORT_ON_OOM
CCOPT= $(CFLAGS)
OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o
BENCHOBJ = ae.o anet.o benchmark.o sds.o adlist.o zmalloc.o
CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o
PRGNAME = redis-server
BENCHPRGNAME = redis-benchmark
CLIPRGNAME = redis-cli
all: redis-server redis-benchmark redis-cli
# Deps (use make dep to generate this)
adlist.o: adlist.c adlist.h
ae.o: ae.c ae.h
anet.o: anet.c anet.h
benchmark.o: benchmark.c ae.h anet.h sds.h adlist.h
dict.o: dict.c dict.h
redis-cli.o: redis-cli.c anet.h sds.h adlist.h
redis.o: redis.c ae.h sds.h anet.h dict.h adlist.h zmalloc.c zmalloc.h
sds.o: sds.c sds.h
sha1.o: sha1.c sha1.h
zmalloc.o: zmalloc.c
redis-server: $(OBJ)
$(CC) -o $(PRGNAME) $(CCOPT) $(DEBUG) $(OBJ)
@echo ""
@echo "Hint: To run the test-redis.tcl script is a good idea."
@echo "Launch the redis server with ./redis-server, then in another"
@echo "terminal window enter this directory and run 'make test'."
@echo ""
redis-benchmark: $(BENCHOBJ)
$(CC) -o $(BENCHPRGNAME) $(CCOPT) $(DEBUG) $(BENCHOBJ)
redis-cli: $(CLIOBJ)
$(CC) -o $(CLIPRGNAME) $(CCOPT) $(DEBUG) $(CLIOBJ)
.c.o:
$(CC) -c $(CCOPT) $(DEBUG) $(COMPILE_TIME) $<
clean:
rm -rf $(PRGNAME) $(BENCHPRGNAME) $(CLIPRGNAME) *.o
dep:
$(CC) -MM *.c
test:
tclsh test-redis.tcl
bench:
./redis-benchmark
log:
git log '--pretty=format:%ad %s' --date=short > Changelog
Redis 的入口
我们直接到 redis 的入口函数,其代码在 redis.c 中,直接拖拽滚动框到最底部即可,我对代码的每个阶段做了注释。启动一个 redis 服务其实 3 个阶段总计 8 个步骤。
阶段一:基本初始化
- 初始化 server 配置,initServerConfig()。
- 判断是否有指定配置文件,如果有则重新加载配置文件,loadServerConfig()。
- 初始化服务,initServer()。
- 判断是否为后台执行,daemonize()。
阶段二:检查 RDB
- 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件,rdbLoad()。
阶段三:事件驱动
- 创建 event-driven 其实就是解析客户端请,并且将 socket fd 放入到事件驱动当中去,aeCreateFileEvent()。
- 执行死循环,接收请求,其实这一步就是我们说 redis 是单线程的原因,aeMain()。
- acMain 判断是否停止,如果停止则删除事件,并退出程序,aeDeleteEventLoop()。
int main(int argc, char **argv) {
//1. 初始化 server 段配置
initServerConfig();
//2. 判断是否有指定配置文件,如果有则重新加载配置文件
if (argc == 2) {
ResetServerSaveParams();
loadServerConfig(argv[1]);
} else if (argc > 2) {
//如果参数过多则报错,显示正确的配置
fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
exit(1);
}
//3. 初始化服务
initServer();
//4. 判断是否为后台执行
if (server.daemonize) daemonize();
redisLog(REDIS_NOTICE, "Server started, Redis version " REDIS_VERSION);
//5. 加载 rdb 文件,当进程退出后会加载以前保存的 rdb 文件
if (rdbLoad(server.dbfilename) == REDIS_OK)
redisLog(REDIS_NOTICE, "DB loaded from disk");
//6. event-driven 其实就是解析客户端请求
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptHandler, NULL, NULL) == AE_ERR)
oom("creating file event");
redisLog(REDIS_NOTICE, "The server is now ready to accept connections on port %d", server.port);
//7. 死循环执行
aeMain(server.el);
//8. 当接收到退出信号后,删除事件
aeDeleteEventLoop(server.el);
return 0;
}
阶段一:基本初始化
initServerConfig
初始化 Server 配置
initServerConfig 就是对 redis 进行默认值的初始化。
static void initServerConfig() {
# 初始化 redis 默认 DB 数量,默认是 16 个
server.dbnum = REDIS_DEFAULT_DBNUM;
# 初始化 redis 默认端口 3306
server.port = REDIS_SERVERPORT;
# 日志级别,默认是 debug
server.verbosity = REDIS_DEBUG;
# 默认超时时间
server.maxidletime = REDIS_MAXIDLETIME;
# 存储策略
server.saveparams = NULL;
server.logfile = NULL; /* NULL = log on standard output */
server.bindaddr = NULL;
# 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
server.glueoutputbuf = 1;
server.daemonize = 0;
server.pidfile = "/var/run/redis.pid";
server.dbfilename = "dump.rdb";
# 是否要求通行证
server.requirepass = NULL;
# 对象共享池
server.shareobjects = 0;
# 重置保存策略
ResetServerSaveParams();
appendServerSaveParams(60 * 60, 1); /* save after 1 hour and 1 change */
appendServerSaveParams(300, 100); /* save after 5 minutes and 100 changes */
appendServerSaveParams(60, 10000); /* save after 1 minute and 10000 changes */
/* 主从复制相关的配置 */
server.isslave = 0;
server.masterhost = NULL;
server.masterport = 6379;
server.master = NULL;
server.replstate = REDIS_REPL_NONE;
}
ResetServerSaveParams() 的操作这里很简单,其实就是将原有的内存空间清空。
static void ResetServerSaveParams() {
zfree(server.saveparams);
server.saveparams = NULL;
server.saveparamslen = 0;
}
在这里,在 redis 0.091 版本中,redis 对内存分配使用的还是 zmalloc,这个我们放在后面去讲解,
server 是一个全局静态变量,数据类型是 redisServer。
loadServerConfig
重新加载配置文件
重新加载配置文件就很好理解了。
if (argc == 2) {
ResetServerSaveParams();
loadServerConfig(argv[1]);
} else if (argc > 2) {
//如果参数过多则报错,显示正确的配置
fprintf(stderr, "Usage: ./redis-server [/path/to/redis.conf]\n");
exit(1);
}
只是一段非常普通的 if 判断。
如果参数是两个,那么就说明有配置文件,执行加载逻辑
- 重新初始化 Server Save 参数
- loadServerConfig
loadServerConfig 由于代码太长了,而且大多数都是参数解析,在这里就不全都贴出来了,我们来看一些核心的逻辑。
//跳过了获取文件,判断文件是否有效的逻辑
while (fgets(buf, REDIS_CONFIGLINE_MAX + 1, fp) != NULL) {
sds *argv;
int argc, j;
linenum++;
line = sdsnew(buf);
line = sdstrim(line, " \t\r\n");
/* Skip comments and blank lines*/
if (line[0] == '#' || line[0] == '\0') {
sdsfree(line);
continue;
}
/* Split into arguments */
argv = sdssplitlen(line, sdslen(line), " ", 1, &argc);
sdstolower(argv[0]);
/* Execute config directives */
if (!strcmp(argv[0], "timeout") && argc == 2) {
server.maxidletime = atoi(argv[1]);
if (server.maxidletime < 1) {
err = "Invalid timeout value";
goto loaderr;
}
} else if (xxxx) {
.....
} else .....
return;
loaderr:
fprintf(stderr, "\n*** FATAL CONFIG FILE ERROR ***\n");
fprintf(stderr, "Reading the configuration file, at line %d\n", linenum);
fprintf(stderr, ">>> '%s'\n", line);
fprintf(stderr, "%s\n", err);
exit(1);
}
在这段,出现了我们 redis 的第一个数据结构——sds,也就是我们在缓存中使用最多的字符串类型。
在 Redis0.091 中,SDS 的实现还是非常简单的,具体的 SDS 操作,我们在后续的部分进行讲解,我们先了解一下这个数据结构。头文件加实现代码一共才 275 行。
Reids 利用 SDS 代替的字符串,这涉及到字符串的二进制安全问题。,C语言中,用“\0”表示字
符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。
在 C 语言中,字符串以 \0 结尾,这也就意味着字符串中如果出现了\0就会破坏数据的完整性,字符串也就会被截断。
为了解决这样的问题,redis 将 sds 设计成下面的样子。
typedef char *sds;
struct sdshdr {
long len;
long free;
char buf[];
};
- len 属性为5,表示这个SDS保存着一个5字节长的字符串,由于长度统计变量的存在,读取字符串不依赖”\0”终止符,保证了二进制安全。
- free 属性为0,表示这个SDS没有分配任何未使用空间。
- buf:内容存储在柔性数组 buf中,SDS 对上层暴露的指针不是指向结构体 SDS 的指针,而是直接只想荣幸数组的指针,上层就可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数。
柔性数组只能放在结构体的尾部。柔性数组的地址和接头体是连续的,这样查找内存更快,因为不需要额外通过指针找到字符串为止,可以方便地通过柔性数组的首地址偏移得到结构体首地址,进而方便获取其余变量。
总结一下 SDS 和 Char 的区别
- 通过使用SDS字符串(len记录字符串长度),使得获取字符串长度的复杂度从O(N)变为O(1)
- 杜绝缓存区溢出,C字符串不记录自身长度,在拼接字符串时可能造成缓存区溢出
- 通过未使用空间free,减少修改字符串带来的内存重分配次数
其实这种设计在其他项目中也存在,这是 PHP3 中对 string 类型的视线。
当校验没有问题后,就开始抓好难过解析配置,如果配置正确就继续扫描下一行,如果错误就会利用 goto 跳转到 loaderr。
可能在很多 C 语言的帖子中或者老师的课程中都不允许使用 goto 关键字,但是在实际的项目中这种用法有很多,并不是绝对不可以使用,因为在 C语言中并没有异常机制,像这种利用 goto 特性跳转到输出位置进行输出信息,处理异常会特别方便,代码逻辑也更加的清晰,在处理异常的同事也可以进行回收资源。
老师说的意思其实是不要多层跳转,因为多层跳转会让代码变得混乱。
initServer()
初始化 server
得到配置后,万事俱备只欠东风,我们开始对 server 进行初始化。
static void initServer() {
int j;
// 屏蔽信号,避免异常
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
server.clients = listCreate();
server.slaves = listCreate();
server.monitors = listCreate();
server.objfreelist = listCreate();
// 创建 SharedObjects 这里就是存储 redis 数据的地方;
// 如果有熟悉Redis的朋友可能会知道 RedisObject,这个就是它的前身。
createSharedObjects();
//创建事件驱动
server.el = aeCreateEventLoop();
server.db = zmalloc(sizeof(redisDb) * server.dbnum);
server.sharingpool = dictCreate(&setDictType, NULL);
server.sharingpoolsize = 1024;
// 如果分配内存失败也就意味着没有可用空间了,那就 OOM
if (!server.db || !server.clients || !server.slaves || !server.monitors || !server.el || !server.objfreelist)
oom("server initialization"); /* Fatal OOM */
#创建 TCP 连接
server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);
if (server.fd == -1) {
redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);
exit(1);
}
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&hashDictType, NULL);
server.db[j].expires = dictCreate(&setDictType, NULL);
server.db[j].id = j;
}
server.cronloops = 0;
server.bgsaveinprogress = 0;
server.lastsave = time(NULL);
server.dirty = 0;
server.usedmemory = 0;
server.stat_numcommands = 0;
server.stat_numconnections = 0;
server.stat_starttime = time(NULL);
aeCreateTimeEvent(server.el, 1000, serverCron, NULL, NULL);
}
- 屏蔽信号
初始化 clients,slaves,monitors,objfreelist 四个链表。
- clients 客户端
- slaves 从库
- monitors 监控器,某个 client 执行 monitors 的时候会变成一个监控器,服务端执行的所有命令都会发送给监控器。
- objfreelist 空闲对象池
- 初始化 dict 也就是 shareobjects 存储键值对的地方
- 创建 tcp 连接
- 创建数据库,其中会初始化 dict,过期时间 dict
- 初始化 server 剩余的属性
- 注册听时期
listCreate()
引出了,我们的第二个数据结构——双向链表。
其代码量加载一起一共是 214 行,核心主要是下面两个结构体,list、ListNode 构成了 redis 中的双向链表。
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned int len;
} list;
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
在链表中保存着 head 和 tail,也就是链表中的头和尾。每个 Node 节点都保存着前一个和后一个的节点指针。
通过单个节点我们不仅能够访问到下一个节点,还能访问到上一个节点。使用双链表而非单链表的目的是想增加结构的灵活性,因为只有经常对数据进行增加或者删除才会用到链表这种数据结构。无论是增加节点抑或删除节点,双链表都要比单链表更加简单高效。但提高灵活性的代价是需要更多的空间,但这个代价小到可以被我们忽略。
daemonize
守护进程
static void daemonize(void) {
int fd;
FILE *fp;
if (fork() != 0) exit(0); /* parent exits */
setsid(); /* create a new session */
/* Every output goes to /dev/null. If Redis is daemonized but
* the 'logfile' is set to 'stdout' in the configuration file
* it will not log at all. */
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}
/* Try to write the pid file */
fp = fopen(server.pidfile, "w");
if (fp) {
fprintf(fp, "%d\n", getpid());
fclose(fp);
}
这是一个标准的守护进程代码。
- fork 子进程,父进程退出
- 设置会话 id
- 转移标准输入,标准输出,标准错误
- 将 pid 放到文件中
阶段二:rdbLoad
加载 rdb 文件
函数loadDb
的实现完全是根据写入磁盘数据库文件的rdbSave
函数来实现的, 后续我们在定时事件中再看rdbSave
实现,在此略过分析loadDb
的实现。
阶段三:事件驱动
事件驱动框架是 Redis Server 运行的核心,该框架爱一单启动后就会一直执循环执行,每次循环都会处理一批出发的网络读写事件。
其实进入事件框架开始执行并不复杂,main 函数直接调用时间框架爱的主题函数 aeMain 后就进入了时间处理循环了。
//7. 死循环执行
aeMain(server.el);
而他的源代码实现更简单。
void aeMain(aeEventLoop *eventLoop)
{
eventLoop->stop = 0;
while (!eventLoop->stop)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
其实就是一个死循环,如果当前的事件驱动的 stop
属性等于 1 了就说明服务终止,那么就退出循环,整个业务逻辑就结束了。
//8. 当接收到退出信号后,删除事件
aeDeleteEventLoop(server.el);
在 0.091 这个版本中还没有引入 epoll 的支持,底层使用的还是 select。
所以在这个版本中,如果没有做内核优化,那么 redis 的并发 fd 最多也就是 1024 个,远没有后面那么高的并发量。
到目前为止,我们算是知道了 redis0.091 的入口,接下来,我们会进入到具体的实现细节当中,下一章,我们来看 redis server 是如何处理用户请求的。