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的事件驱动模型,整个事件循环的逻辑过程都没有涉及具体的命令操作,只需要定义事件的类型和处理器即可,实现接收事件和处理流程相互解耦。