Redis系列5-事件驱动模型+IO多路复用机制
Redis中所有命令执行都是基于事件驱动模型的,结合IO多路复用机制,实现单进程单线程的高并发快速访问。
Redis中的事件包含文件事件和时间事件,文件事件基于客户端和Redis服务端建立Socket连接产生的事件,是对Socket连接的一个抽象概念。而时间事件指的是Redis服务端需要的定时出发的事件,类似于定时任务。
一、Redis 事件驱动模型
1.1 事件驱动模型
事件驱动的意思是只有发生某个事件时,程序才会有所行动。事件驱动模型在架构设计领域被称为Reactor 模式,体现的是一种被动响应的特征。模型流程图如下
主程序处于一个阻塞状态的事件循环(event loop)中等待事件(event),当有事件发生时,根据事件的属性分发到相应的处理函数进行处理。事件以并发的方式发送到服务处理器 (service handler),服务处理器将事件整合到一个有序队列中(这过程称为demultiplexes),并分发到具体的请求处理器 (request handler)进行处理。
1.2 Redis 核心原理
Redis 事件模型如下图所示:
当Socker连接可读或者可写时,就把对应的事件加入到事件待处理队列fired中,以有序 (sequentially)、同步 (synchronously) 的方式发送给事件处理器进行处理。这个过程在Redis中被称为Fire。最后由事件对应的处理器(Handler)将处理的结果返回给客户端去。
通过以上概括了Redis 处理用户请求的大致过程。从这个过程我们可以发现:
Redis 处理所有命令都是顺序执行的,其中包括来自客户端的连接请求。所以当 Redis 在处理一个复杂度高、时间很长的请求(比如 KEYS 命令)的时候,其他客户端的连接都没办法相应。
Redis 内部定时执行的任务也是放在顺序队列中处理,其中也可能包含时间较长的任务,比如自动删除一个过期的大 Key,比如很大 list, hash, set 等。所以有时候会遇到明明业务没有主动操作复杂,但也会出现卡顿的问题。
1.2.1 什么是 IO 多路复用 (multiplexing)?
I/O多路复用是一种机制,通过监视多个描述符,一旦某个描述符就绪,就通知程序进行相应的操作。多路-指的是多个socket连接,复用-指的是复用一个线程。
epoll提供的3个方法:
/*
* 创建一个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);
//事件状态
typedef struct aeApiState {
// epoll_event 实例描述符
int epfd;
// 事件槽
struct epoll_event *events;
} aeApiState
//创建一个新的epoll;对 epoll.epoll_create() 的封装。
static int aeApiCreate(aeEventLoop *eventLoop)
//调整事件槽的大小
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
// 释放 epoll 实例和事件槽
static void aeApiFree(aeEventLoop *eventLoop)
//关联给定事件到 fd;对 epoll.epoll_ctl()的封装。
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
//从 fd 中删除给定事件;对 epoll.epoll_ctl()的封装。
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
//拉取可执行事件;对 epoll_wait()的封装。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
1.3 事件驱动模型的优势
-
有利于架构解耦和模块化开发,使功能架构实现上更加解耦,模块的可重用性更高。因
事件循环的流程本身和具体的处理逻辑之间是独立的,只要在创建事件的时候关联特定的处理逻辑(事件处理器),就可以完成一次事件的创建和处理。 -
有利于减小高并发量情况下对性能的影响,相比一个连接分配一个线程的模型, Reactor 模式(固定线程数)在连接数增大的情况下吞吐量不会明显降低,延时也不会也受到显著的影响。
二、事件循环的 Redis 实现
2.1 Redis 事件循环 Event Loop
Redis实现事件循环主要涉及三个源码文件:server.c, ae.c, networking.c。
server.c的main()函数是整个Redis程序的开始,这里初始化redis的配置,创建时间循环对象EventLoop,同时注册一个服务端可读事件,等待客户端连接。ae.c实现事件循环和事件的相关功能。networking.c则负责处理网络IO相关的功能。
事件循环流程如下图:
//main方法中初始化执行3个步骤:
//1.加载配置 2.创建时间循环对象并注册可读事件 3.执行事件循环
// 0. 定义服务器主要结构体, 加载服务器配置
struct redisServer server;
initServerConfig();
loadServerConfig();
// 1. 根据配置参数初始化,
initServer() {
// 1.1 实际创建事件循环
server.el = aeCreateEventLoop();
// 1.2 创建一个关联了acceptTcpHandler处理器的可读事件,
//用于响应外部客户端请求,实现Redis对外提供的服务地址和端口的连接服务。
aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)
}
//2. 执行事件循环,等待连接和命令请求
aeMain(server.el);
//初始化过程中被创建的server.el包含了两个事件的列表,它的结构体实现如下:
typedef struct aeEventLoop
{
aeFileEvent events[AE_SETSIZE]; /* 注册的事件,被 eventloop 监听 */
aeFiredEvent fired[AE_SETSIZE]; /* 有读写操作需要执行的事件(就绪事件) */
} aeEventLoop;
ae.c文件中的aeMain()实现事件循环的执行,简化后的代码如下:
//事件循环主要就是一个while循环,不断去轮询是否有就绪的事件需要处理,具体的处理函数是aeProcessEvents
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
c. networking.c文件中,综合调度器(aeProcessEvents)是 Redis 统一处理所有事件的地方。
//从 epoll 中获关注的事件
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 从已就绪数组中获取事件 fe就是要处理的文件事件 file event
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++;
}
...
//最后一步:如果有时间事件,则进行时间事件的处理:
processTimeEvents(eventLoop);
2.2 事件处理器Event Handler
事件
被创建时,都会关联一个
处理器 (handler)
,并注册到
事件循环
中,事件处理器用于具体的读写操作。
Redis的常
用
几个事
件处理器
有:
事件
,并注册到
事件循环
中进行监听,Redis 将
处理器
以参数的方式关联到事件中。
比如以下是注册一个可读事件的操作:
/**
* server.el:事件循环 eventloop,一个服务器只有一个el
* fd:表示这个客户端连接的文件描述符,每个客户端连接对应一个
* AE_READABLE:表示这是一个可读事件,可以理解为客户端准备进行写操作
* readQueryFromClient:这个事件关联的处理器,当事件就绪后,就会调用此处理器
* c:表示这个客户端在Redis中指向的变量
**/
aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)
//注册完毕后,事件循环就会将这个事件(套接字)加入到监听的范围,当事件可读时,Redis就会将这个事件
//发送到待处理事件队列中等待处理,等到可读就绪时,会被readQueryFromClient处理器处理。
至此,Redis 的事件循环的机制已经介绍完毕,可以观察到整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可。可以说这部分就是Reactor 模式体现出来的一个好处:接收事件和处理流程的实现相互解耦。
三、一次命令操作的完整流程
本章是建立在 Redis 已经完成了初始化工作,主要是创建事件循环之后,Redis 接受一个客户端操作的完整流程的介绍。
本章主要分为两个阶段:
第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。
第二阶段:已经建立连接的客户端,对Redis 发起一次
SET命令的操作。
3.1 一个客户端连接进服务器的过程
如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。
当有客户端连接到 Redis 服务器的时候,注册在事件循环中的监听服务端口的事件就会变成读就绪状态,从而触发这个事件到待处理事件队列中,准备调用acceptTcpHandler进行处理。
为在服务器端创建一个对应本次连接的套接字。
把服务端套接字的文件描述符
cfd作为参数,创建client变量。为该客户端连接创建并注册一个关联了
readQueryFromClient处理器的可读事件到事件循环,用于下一步接收并执行命令的工作。
3.2 一次客户端连接和调用命令的执行流程
可读事件
关联到
事件循环
,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调
readQueryFromClient
处理器,对用户发送过来的请求,按 RESP (REdis Serialization Protocol) 进行解析处理后,调用相关的命令进行处理。
调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的
SET(2)调用该命令关联的函数进行处理,这里就是setCommand。setCommand函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。回复客户端
addReply函数将返回给客户端的内容,写到客户端变量的输出缓冲client.buf中,等待发送给客户端。
返回结果给客户端
以上是整个SET命令的事件处理,不过在这个时候,返回给用户的回复内容,只存放于服务器的客户端变量输出缓冲中。至于将结果返回给用户的过程,取决于版本,有不同的操作。
在 4.0 以前,每次的addReply操作会创建一个写事件,然后放到事件循环中执行。
而 4.0 开始,在每次重新进入一个新的循环之前,就是eventLoop->beforesleep();这个操作,Redis 会尝试直接发送给客户端,只有当发送的内容超过一定大小,无法一次发送完成的时候,才会去创建一个可写事件。
有兴趣的读者可以去看下 Redis 作者的这个 commit:
antirez in commit 1c7d87d: Avoid installing the client write handler when possible.
目的是减少一次系统调用,适用于大部分操作类命令的回复。
Redis为什么单线程能承载高并发?
5.Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
四、总结
Redis采用IO多路复用技术,将多客户端的Socket连接、读、写转化为服务端文件的可读可写事件,并采用单线程循环的方式去拉取已就绪的事件,然后找到事件对应的处理器执行相应的处理操作,使用单线程减少了线程的上下文切换和竞争。这就是Redis的事件驱动模型,整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可,实现接收事件和处理流程相互解耦。
