Redis 单机数据库设计与源码赏析
Redis 是一个开源的、基于内存的数据结构存储系统,可以作为数据库、缓存和消息中间件。在各大厂商都有广泛的应用。本篇文章是对 Redis 单机数据设计与实现的学习总结。
主流程
本节主要分析 Redis 启动的主流程,对其工作流程有一个整体的了解。首先从主函数 main() 开始。注:Redis 的源码版本为 6.0.1。
主函数
直接上源码:
// server.c 4917
int main(int argc, char **argv) {
...
initServerConfig();
loadServerConfig(configfile,options);
initServer();
aeMain(server.el);
retunr 0;
}
主函数是 redis 服务启动的入口,主要进行以下操作:

初始化 - 默认配置
初始化一个 Redis 服务器变量 server 保存服务器的全局状态。server 的数据结构为 redisServer ,如下:
// server.h:1029
struct redisServer {
redisDb *db;
int dbnum;
dict *commands;
aeEventLoop *el;
int port;
}
初始化 server 默认配置。进行通用配置 (默认端口号、配置文件路径)、复制、集群默认、Lua 脚本、保存策略、命令表等基础配置。
// server.c: 2288
void initServerConfig(void) {
// 初始化配置文件
server.configfile = NULL;
// 初始化日志文件
server.logfile = zstrdup(CONFIG_DEFAULT_LOGFILE);
// 主从复制配置
server.masterhost = NULL;
..
}
初始化 - 命令表
初始化命令表,比如 get、set、hset 等各自的处理函数,通过 hash 表进行存储,应用于后续处理请求。
Redis 中命令处理函数用 redisCommand 表示,如下
// server.h 1445
struct redisCommand {
char *name; // 名称
redisCommandProc *proc; // 处理函数
// ...
}
Redis 在代码中预设命令表 redisCommandTable ,如下。
// server.c 182
struct redisCommand redisCommandTable[] = {
// ...
{"get",getCommand,2,
"read-only fast @string",
0,NULL,1,1,1,0,0,0}, // GET 命令
// ...
}
在 initServerConfig() 函数中为 redisServer 的命令表变量 commands 进行初始化,如下。
// server.c 2384
void initServerConfig(void) {
server.commands = dictCreate(&commandTableDictType,NULL); // 初始化命令表
populateCommandTable(); // 填充命令表
// ...
}
// server.c 2982
void populateCommandTable(void) {
for (j = 0; j < numcommands; j++) {
struct redisCommand *c = redisCommandTable+j;
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
}
}
初始化 - 数据库
Redis 数据库的结构体为 redisDb 。如下:
// server.h 643
typedef struct redisDb {
dict *dict; // 数据库键空间,保存所有的键值对
dict *expires; // 键的过期时间
dict *blocking_keys; // 处于阻塞状态的键
dict *ready_keys; // 处于就绪状态的键
dict *watched_keys; // 被 watch 命令监视的键
int id; // 数据库 ID
long long avg_ttl; // 评价 TTL
}
创建 Redis 数据库,默认创建 16 个数据库对象。
void initServer(void) {
// server.c 2750
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
// server.c 2778
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
...
}
}
初始化 - 共享对象
Redis 基于数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。通过以上对象对数据进行存储等操作。
Redis 中的每个对象都由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding 属性和 pt r属性。如下:
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; //
unsigned lru:LRU_BITS; // 对象所使用的编码
int refcount; // 引用计数
void *ptr; // 指向对象底层实现数据结构
} robj;
redisObject 通过引用计数的内存回收机制,当不再使用某个对象的时候进行内存的自动释放操作。另外基于引用计数机制,可以通过共享对象来节约内存。在初始化时,还会创建一些共享变量来节约内存的使用,比如创建 10000 个整数对象。如下:
// server.c 2173
void createSharedObjects(void) {
// 客户端和服务器发送的命令 (CRLF)结尾
shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
shared.err = createObject(OBJ_STRING,sdsnew("-ERR\r\n"));
// 创建 10000 个共享整数
for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
shared.integers[j] =
makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
shared.integers[j]->encoding = OBJ_ENCODING_INT;
}
}
初始化 - 事件循环
"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"
在 Redis 进入循环事件之前,会先初始化 aeEventLoop 对象。aeEventLoop 具有两个作用:(1) 保存待处理的文件事件合时间事件;(2) 选择适合的 I/O 多路复用程序的实现。
// server.c
void initServer(void) {
// 2743
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
}
// ae.c 63
aeEventLoop *aeCreateEventLoop(int setsize) {
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (aeApiCreate(eventLoop) == -1) goto err;
}
以 epoll 作为例子,介绍 Redis 对 I/O 多路复用的封装。首先 aeApiCreate 函数,会初始化 aeApiState 对象,初始化了epoll就绪事件表,然后调用 epoll_create 创建 epoll 实例,最后赋值给 aeEventLoop 。
// ae_epool.c 39
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
state->epfd = epoll_create(1024);
eventLoop->apidata = state;
}
Redis 此外还封装了 epoll 的其他几个函数:
- aeApiCreate
- aeApiAddEvent,使用
epoll_ctl向epfd中添加需要监控的 FD 以及监听的事件 - aeApiPoll,将
epoll_event数组中存储的信息加入eventLoop的fired数组中,将信息传递给上层模
// ae.c 47
// 选择性能最好的 I/O 多路复用实现
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
初始化 - 注册连接处理器

当 Redis 客户端连接服务器的时候,Redis 服务器监听的套接字准备好执行连接应答 (accept) 操作,会调用事前关联好的连接处理器进行处理。在初始化的阶段会初始化文件事件描述符 server.ipfd,来监听 server 配置的地址与端口。然后为对应的文件描述符注册连接处理器。
// 监听地址与端口,生成文件描述符
// server.c
void initServer(void) {
// 2754
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
}
// server.c 2600
int listenToPort(int port, int *fds, int *count) {
fds[*count] = anetTcpServer(server.neterr,port,server.bindaddr[j],server.tcp_backlog);
}
为文件描述符注册连接处理器。
// server.c
void initServer(void) {
// 2847
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL)
}
}
aeCreateFileEvent 接受一个套接字描述符、一个事件类型,以及事件处理器作为参数,将给定的套接字的给定事件加入到 I/O 多路复用程序的监听范围,并对事件处理器进行关联。
// ae.c 153
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{
aeFileEvent *fe = &eventLoop->events[fd];
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
}
开启事件循环

Redis 通过 aeMain() 函数进入事件主循环。当时间事件与文件事件需要被处理的时候,循环事件就会唤起各自的事件处理器。在 aeProcessEvents() 中概括了对应的处理逻辑,时间事件被自定义的逻辑唤起,文件事件在 I/O 多路复用程序通知的时被处理。在 beforesleep(eventLoop) 中会进行发送响应给客户端、刷新 AOF文件等操作。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}