阅读Redis源码是一个学习C语言,了解内存模型、网络编程、key-value数据库的有效途径。有关redis源码的博客汗牛充栋,结合它们去阅读源码可以更好的理解。

《Redis设计与实现》也是一本很棒的Redis源码讲解书,和很多博客相似,它们都是从Redis的底层数据结构讲起,自底向上,层层解析。此篇博客我想按照自顶向下的顺序来讲解,以对源码的每一个文件有一个快速的认识。

文件事件

服务端的main函数位于server.c,主函数会调用 aeMain() ,这个 aeMain() 定义于 ae.c 文件中,是一个while循环,轮询处理文件事件和时间事件。

//ae.c aeMain()
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

所以关键是 eventLoop 和aeProcessEvents() 函数。

我们来看看 aeEventLoop 结构:

//ae.h 
typedef struct aeEventLoop {
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    time_t lastTime;     /* Used to detect system clock skew */
    aeFileEvent *events; /* Registered events */
    aeFiredEvent *fired; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc; 
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent;

typedef struct aeFiredEvent {
    int fd;
    int mask;
} aeFiredEvent;

Redis是一个事件驱动的服务器,Redis事件有文件事件和时间事件,对应上面的 aeFileEvent 结构和 aeTimeEvent 结构。文件事件就是服务器对套接字操作的抽象,Redis的IO复用方式可以是 select poll epoll ,编译时会选择其中一种方式,文件事件将文件描述符和相应的回调函数放到一个结构体里,这样上层函数处理文件事件时就不用关心底层的实现了。

比如说,select 和 epoll 在阻塞函数返回后判断文件描述符是否就绪的方式不同,select 需要遍历整个文件描述符集合,用 FD_ISSET() 来判断,而 epoll 只需要遍历就绪文件描述符集合就行了。Redis对它们整合的方法是,aeEventLoop 结构有一个文件描述符集合 events ,和就绪文件描述符集合 fired ,对于上层的方法,在IO复用函数返回后,就绪的fd都会放进 fired 中,所以只需要遍历 fired 就行了,而不用关心底层的实现。

//ae.c aeCreateFileEvent()
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

Reactor模型

首先,我们来分析一下,client端敲出的SET指令如何通知server的,这涉及到网络编程的知识。整体上来说,不管上层函数采用什么方式,什么架构,底层还是对 accept() listen() read() write() 这些常用函数的封装。

其实,在阅读源码的过程中,我完全没有意识到Reactor的存在,这篇博文讲得很好,(高性能网络编程6–reactor反应堆与定时器管理),Reactor模型更多的是从软件工程的角度去考虑的,我们先从局部代码看起,然后再上升到整体模型的高度。

现在我们重新理一下服务端的流程,server会创建一些(不是一个) socket 监听某些ip地址,在服务器初始化时,会给监听socket创建文件事件,用户建立连接后,会执行该文件事件中的回调函数。核心代码如下:

//server.c initServer()
void initServer(void){
    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                serverPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
}

这个 acceptTcpHandler 就是用户连接后的回调函数,它会调用 createClient() 创建用户,以及封装 accept() 。createClient() 中有很重要的一行,aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) ,给每个建立连接的文件描述符添加读事件的响应函数 readQueryFromClient 。这个函数会封装read() 读取用户数据,采用redis协议解析用户数据并生成对应的命令并传给processCommand() 方法。

我们来看一下Redis的Reactor网络模型

可以看到,之前所说的 readQueryFromClient()就是一种文件事件处理器,此外还有:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask)
void acceptUnixHandler(aeEventLoop *el, int fd, void *privdata, int mask)
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask)
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)

processCommand

之前说到的函数 processCommand ,就是要具体处理客户端传来的命令了。它主要执行了以下步骤:

  • 检查命令合法性,将命令存储到 client 中
  • 如果是集群模式,会通过一致性hash算法转向其它redis服务器
  • 检测占用内存,如果超过限制会执行 freeMemoryIfNeeded() ,它会根据一些策略(例如lru策略)删除一些键(例如过期键),直到占用内存在限制之内
  • 如果是从数据库,在某些设置下不能写入数据
  • 执行 call()

call() 是Redis执行命令的核心函数。它会执行诸如发送命令到从数据库,修改aof文件等等,关键的异步是从 redisCommand 中取得命令名称对应的函数,并执行该函数。redisCommand 结构体存储了Redis的所有命令名称、执行函数和一些标志位。

就整体而言,Redis数据库是一个大字典,查询操作需要根据用户数据中的键名,寻找到对应的存储数据,插值操作则是给这个大字典加一个新键。

存取某一数据类型的用户命令会调用相应类型的 command 函数,且定义在t_xx.c这样的源文件中,例如 set 命令会调用 setCommand() 函数,因为这个命令是用来修改 string 的,所以该函数在 t_string.c 源文件定义。

对象与编码

Redis有5种基本数据类型(或者说,对象),sds(简单动态字符串) list dict set zset ,每种数据类型又可以有不同的编码方式。Redis设计出这些数据结构和编码方式不仅仅是存储用户数据,代码中的很多变量也都是使用这些结构。例如之前所说的解析用户命令,最终就是以sds结构存储的。个人觉得,各种数据结构的设计首要目的都是为了节省内存。

其他

此外还有订阅发布、持久化、集群、主从复制等等特性,以及Redis4.0新增的aof-rdb混合模式、模块系统、内存统计等等,限于篇幅没有介绍,这些都是Redis的闪光点,有兴趣的童鞋可以继续深入了解。