Redis是一个事件驱动的内存数据库,服务器需要处理两类事件
- 文件事件:
Redis服务器通过
Socket
套接字与客户端(或其它Redis服务器)进行连接交互,Redis服务器
通过监听这些Socket
产生的文件事件并处理这些事件实现对客户端的相应,完成客户端需要的操作。
- 时间事件
Redis服务器的某些操作需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的对象。
1. 文件事件
1.1 Reactor
Redis基于Reactor模式开发了自己的事件处理器,这个处理器被称为文件事件处理器。它的模式流程图如下所示:

图解:
- I/O多路复用模块会监听多个FD,也就是多个套接字,当这些FD产生
accept、read、write或者close
的文件事件,会向事件分发器传送事件。 - 事件分发器收到事件之后,根据时间的类型把时间发给对应的处理器
1.2 I/O多路复用模块的实现
Redis的I/O多路复用模块的功能都是封装了操作系统提供的select、epoll、evport、kqueue
这些基础函数。向上层提供了一个统一的接口,屏蔽了底层实现的细节。每个I/O多路复用函数库在Redis源码中都对应一个单独的文件。
例子,redis利用linux提供的epoll实现I/O多路复用:
首先,epoll提供的三个方法
/*
* 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
*/
int epoll_create(int size);
/*
* 增删改 fd 需要监听的事件
* epfd 是 epoll_create() 创建的句柄。
* op 表示 增删改
* epoll_event 表示需要监听的事件,Redis 只用到了可读,可写,错误,挂断 四个状态
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
* 查询符合条件的事件
* epfd 是 epoll_create() 创建的句柄。
* epoll_event 用来存放从内核得到事件的集合
* maxevents 获取的最大事件数
* timeout 等待超时时间
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
再看下Redis对文件事件,封装epoll
向上提供的接口
/*
* 事件状态
*/
typedef struct aeApiState {
// epoll_event 实例描述符
int epfd;
// 事件槽
struct epoll_event *events;
} aeApiState;
/*
* 创建一个新的 epoll
*/
static int aeApiCreate(aeEventLoop *eventLoop)
/*
* 调整事件槽的大小
*/
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
/*
* 释放 epoll 实例和事件槽
*/
static void aeApiFree(aeEventLoop *eventLoop)
/*
* 关联给定事件到 fd
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
/*
* 从 fd 中删除给定事件
*/
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
/*
* 获取可执行事件
*/
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
ae_peoll.c对epoll进行分一些封装:
-
aeApiCreate()
是对epoll.epoll_create()
的封装。 -
aeApiAddEvent()
和aeApiDelEvent()
是对epoll.epoll_ctl()
的封装。
-aeApiPoll()
是对epoll_wait()
的封装。
这样的话再看Reids多路复用函数中的epoll的实现就很清楚了,再往上看下ea.c的封装。除了使用ae_peoll.c提供的方法外,ae.c还增加了"增删改"的API。
先看下事件处理器的数据结构
typedef struct aeFileEvent {
// 监听事件类型掩码,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
//直接理解为时间的类型即可
int mask; /* one of AE_(READABLE|WRITABLE) */
// 读事件处理器
aeFileProc *rfileProc;
// 写事件处理器
aeFileProc *wfileProc;
// 多路复用库的私有数据
void *clientData;
} aeFileEvent;
1.3事件分发器
Redis的事件分发器不但可以处理分发文件事件还可以处理分发时间事件,它根据上面mask
类型调用不同的事件处理器,源码如下:
//从 epoll 中获关注的事件
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 从已就绪数组中获取事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
// 读事件
if (fe->mask & mask & AE_READABLE) {
// rfired 确保读/写事件只能执行其中一个
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
1.4 文件事件处理器的类型:
Redis中有大量的事件处理器类型,我们这里先说图里的三个
-
readQueryFromClinet 命令请求处理器
:负责读取通过socket
发来的命令 -
sendReplyToClient 命令回复处理器
:当Redis处理完命令,就会产生AE_WRITEABLE
事件,将数据回复给Client
-
acceptTcpHandler 连接应答处理器
:负责处理连接相关的事件,当有Client
连接到此Redis
服务的时候,产生AE_READABLE
事件。引发它的执行。
2 . 时间事件
Redis有很多操作需要在给定的时间点进行执行,时间事件就是对这类定时任务的抽象。
时间事件的数据结构如下:
/* Time event structure
*
* 时间事件结构
*/
typedef struct aeTimeEvent {
// 时间事件的唯一标识符
long long id; /* time event identifier. */
// 事件的到达时间
long when_sec; /* seconds */
long when_ms; /* milliseconds */
// 事件处理函数
aeTimeProc *timeProc;
// 事件释放函数
aeEventFinalizerProc *finalizerProc;
// 多路复用库的私有数据
void *clientData;
// 指向下个时间事件结构,形成链表
struct aeTimeEvent *next;
} aeTimeEvent;
从数据结构代码可以看到,它是一个链表的结构,按照id倒序排列的链表。id与时间相关的。

processTimeEvent
Redis使用这个函数处理所有的时间事件,执行思路:
- 记录最新一次执行这个函数的时间用于处理系统时间被修改产生的问题
- 遍历列表找出所有的
when_sec
和where_ms
小于现在时间的事件- 执行时间对应的处理函数
- 检查事件类型,如果是周期事件则刷新该时间下一次的执行时间
- 否则从列表中删除事件(被执行过且不需要再次执行了)
综合调度器(aeProcessEvents)
综合调度器是Redis统一处理所有事件的地方,伪代码如下:这是Redis事件处理器的逻辑
// 1. 获取离当前时间最近的时间事件
shortest = aeSearchNearestTimer(eventLoop);
// 2. 获取间隔时间
timeval = shortest - nowTime;
// 如果timeval 小于 0,说明已经有需要执行的时间事件了。
if(timeval < 0){
timeval = 0
}
// 3. 在 timeval 时间内,取出文件事件。
numevents = aeApiPoll(eventLoop, timeval);
// 4.根据文件事件的类型指定不同的文件处理器
if (AE_READABLE) {
// 读事件
rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 写事件
if (AE_WRITABLE) {
wfileProc(eventLoop,fd,fe->clientData,mask);
}
看下谁调用的aeProcessEvents
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
eaMain
的调用在Redis的main
方法中找到的
int main(int argc, char **argv) {
//一些配置和准备
...
aeMain(server.el);
//结束后的回收工作
...
}
这个时候思路就有了:
1 . Redis的main()
方法执行了一些配置和准备之后就调用eaMain()
这个方法。
-
eaMain()
中 while(true)的调用aeProcessEvents()
这个方法。
所以我们说Redis是一个事件驱动的程序,并且没fork任何线程,所以也成为基于事件驱动的单线程应用。
小结
来一个面试题来结束这篇文章:
问1:为啥这么快?
答:
- 完全基于内存,绝大部分请求是纯粹的内存操作,数据在内存的字典中,字典的结构类似于HashMap,查找和操作的时间复杂度都是
O(1)
- 数据结构简单,对数据结构的操作也简单,Redis基于C语言精心设计了很多数据结构。
- 采用了单线程,避免了不必要的上下文切换和竞态条件,也不存在多线程切换消耗CPU,不用考虑各种并发锁的问题,也不会出现死锁导致性能的消耗。
- 使用I/O多路复用模型,非堵塞的IO
问2: 说一说I/O多路复用:
答:
多路指的是多个网络连接,复用指的是复用同一个线程。采用多路I/O复用可以让单个线程高效处理多个连接请求,且Redis内存操作速度非常快,也就是说内存的操作不会成为影响Redis
性能的瓶颈,所以它具备很高的吞吐量。
问3:为什么Redis是单线程的?
答:
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。但是有个缺点,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!而且这个单线程指的是接收网络请求只用一个线程处理,实际上还会有一些子线程的,比如刷盘、主从同步、交换心跳信息等
参考
《Redis设计与实现》
https://www.xilidou.com/2018/03/22/redis-event/
网友评论