阅读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; }
我们再来看看 aeProcessEvents()
函数,这个函数负责处理文件事件和时间事件。 它调用aeApiPoll()
捕获激活的socket,并遍历 fired 对这些 socket 进行读写操作。
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网络模型
Redis网络模型
可以看到,之前所说的 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的闪光点,有兴趣的童鞋可以继续深入了解。
作者:朱明代月
链接:https://www.jianshu.com/p/cea30cf110bc
共同学习,写下你的评论
评论加载中...
作者其他优质文章