Redis源码解析1 - 程序框架
前言
(转载请注明出处:http://www.cnblogs.com/curve/archive/2012/09/18/2683226.html)
Redis(REmote DIctionary Server)是一个由Salvatore Sanfilippo写的key-value存储系统。
它有以下特点:
- 性能极高 – Redis能支持超过 100K+ 每秒的读写频率
- 丰富的数据类型 – Redis支持二进制兼容的 string、list、set、zset、hash 等数据结构
- 原子 – Redis的所有操作都是原子性的
- 事务 - 支持多条指令合并成事务执行
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
- 脚本 - 新版本Redis支持lua脚本,可以实现更复杂的逻辑
总体结构
Redis是一个单线程的服务器(除了写磁盘会开子进程,VM管理会开线程,先忽略这两块)
所以,它的服务器启动流程非常清晰,看下图,不再赘述
事件循环
1. 数据结构
通过 aeEventLoop 结构来表示一个事件框架,分析如下:
1 typedef struct aeEventLoop { 2 int maxfd; // 最大句柄号,只有select模型会用到 3 long long timeEventNextId; // timer事件的ID,初始化为0,使用时递增 4 aeFileEvent events[AE_SETSIZE]; // 所有注册的事件,通过aeCreateFileEvent函数增加,aeDeleteFileEvent函数删除。注意,该数组以 fd 为下标进行存取 5 aeFiredEvent fired[AE_SETSIZE]; // 当前帧激活事件,aeApiPoll函数将活跃事件加入到该数组。注意,该结构只保存fd,通过fd到events中取实际event指针 6 aeTimeEvent *timeEventHead; // timer事件链表 7 int stop; 8 void *apidata; /* This is used for polling API specific data */ 9 aeBeforeSleepProc *beforesleep; 10 } aeEventLoop;
2. 事件函数分发流程
redis支持 epoll、kqueue、select 三种网络模型
网络框架的代码实现主要在以下几个文件中:
ae.c / ae.h // 网络框架
ae_epoll.c // epoll模型的实现
ae_kqueue.c // kqueue模型的实现
ae_select.c // select模型的实现
程序选择哪一种,是在编译期确定的
1 1 file ae.c 2 2 3 3 #ifdef HAVE_EPOLL 4 4 #include "ae_epoll.c" 5 5 #else 6 6 #ifdef HAVE_KQUEUE 7 7 #include "ae_kqueue.c" 8 8 #else 9 9 #include "ae_select.c" 10 10 #endif 11 11 #endif
框架函数非常简单,从初始化到结束,主要的函数就3个
aeCreateEventLoop、aeMain、aeDeleteEventLoop
其中,aeMain是事件循环的主体函数,它又会调用 aeProcessEvents函数
三个主体函数会调用 aeApiCreate、aeApiPool、aeApiFree三个接口函数进行处理
这三个接口函数又会映射到具体的某一种网络模型中,而这是在编译期确定下来的
具体如下图所示:
添加删除事件,由
aeCreateFileEvent、aeDeleteFileEvent函数完成,其函数分发流程如下图:
2. 服务端socket建立流程
1. 在服务器初始化时,建立侦听socket,并绑定IP、Port
支持 TCP连接 与本机的unixSocket连接
1 if (server.port != 0) { 2 server.ipfd = anetTcpServer(server.neterr,server.port,server.bindaddr); 3 ... ... 4 } 5 if (server.unixsocket != NULL) { 6 unlink(server.unixsocket); /* don't care if this fails */ 7 server.sofd = anetUnixServer(server.neterr,server.unixsocket,server.unixsocketperm); 8 ... ... 9 }
2. 侦听socket绑定到事件句柄
1 if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, 2 acceptTcpHandler,NULL) == AE_ERR) oom("creating file event"); 3 if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE, 4 acceptUnixHandler,NULL) == AE_ERR) oom("creating file event");
其中“acceptTcpHandler”、“acceptUnixHandler” 是事件回调函数
当新连接到达时,会触发进入这两个函数
3. 再来看看 accept***Handler 里做了什么
1 void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 2 int cport, cfd; 3 char cip[128]; 4 REDIS_NOTUSED(el); 5 REDIS_NOTUSED(mask); 6 REDIS_NOTUSED(privdata); 7 8 // cfd是新连接产生的 socket 句柄 9 cfd = anetTcpAccept(server.neterr, fd, cip, &cport); 10 if (cfd == AE_ERR) { 11 redisLog(REDIS_WARNING,"Accepting client connection: %s", server.neterr); 12 return; 13 } 14 redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport); 15 acceptCommonHandler(cfd); // 继续到这个函数里创建redisClient 16 }
acceptCommonHandler中做的事很简单,调用 CreateClient函数,创建新的redisClient对象
1 static void acceptCommonHandler(int fd) { 2 redisClient *c; 3 if ((c = createClient(fd)) == NULL) { 4 redisLog(REDIS_WARNING,"Error allocating resoures for the client"); 5 close(fd); /* May be already closed, just ingore errors */ 6 return; 7 } 8 ... ... 9 }
在 createClient函数中,将新连接绑定到事件循环中
1 redisClient *createClient(int fd) { 2 redisClient *c = zmalloc(sizeof(redisClient)); 3 c->bufpos = 0; 4 5 anetNonBlock(NULL,fd); 6 anetTcpNoDelay(NULL,fd); 7 if (aeCreateFileEvent(server.el,fd,AE_READABLE, 8 readQueryFromClient, c) == AE_ERR) // 在这里注册了Read事件的回调处理函数 9 { 10 close(fd); 11 zfree(c); 12 return NULL; 13 }
当连接上有数据到达时,便会触发 readQueryFromClient函数,进行实际的网络数据读取与处理
3. Timer事件
Redis将所有Timer绑定到事件循环中进行处理
通过函数 aeCreateTimeEvent 创建新的Timer事件
每一帧事件循环中,通过processTimeEvents函数进行处理
1 static int processTimeEvents(aeEventLoop *eventLoop) { 2 int processed = 0; 3 aeTimeEvent *te; 4 long long maxId; 5 6 te = eventLoop->timeEventHead; 7 maxId = eventLoop->timeEventNextId-1; 8 while(te) { 9 long now_sec, now_ms; 10 long long id; 11 12 // 请注意这一处条件判断 13 // 它的作用是防止死循环,限定当前帧只处理以前帧加入的Timer事件 14 if (te->id > maxId) { 15 te = te->next; 16 continue; 17 } 18 aeGetTime(&now_sec, &now_ms); 19 if (now_sec > te->when_sec || 20 (now_sec == te->when_sec && now_ms >= te->when_ms)) 21 { 22 int retval; 23 24 id = te->id; 25 retval = te->timeProc(eventLoop, id, te->clientData); 26 processed++; 27 28 ... ...
从代码里可以看出,Redis中对Timer的处理是非常粗糙的,并没有用传统的小根堆之类的数据结构
而是简单的用一个链表
这可能是因为Redis中对Timer的依赖程度较低,事件很少
不像 libevent 等通用库,允许用户自由添加timer,无法控制数量,所以要用较高效的数据结构