Redis 主流程赏析

206 阅读6分钟

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_ctlepfd 中添加需要监控的 FD 以及监听的事件
  • aeApiPoll,将 epoll_event 数组中存储的信息加入 eventLoopfired 数组中,将信息传递给上层模
// 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);
    }
}