Redis设计与实现 笔记 第十二章 事件

事件

本章介绍了 Redis 驱动的两大事件, 文件事件 和 时间事件.

文件事件(file event): Redis 通过套接字与客户端进行连接,而文件时间就是服务器对套接字的抽象.服务器与客户端的通信会产生相应的文件时间,而服务器则通过监听并处理这些时间来完成一系列网络通信操作.

时间时间(time event): Redis 服务器中的一些操作(serverCron) 需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象.

12.1 文件事件

Redis 基于 reactor 模式,开发了网络事件处理器,这个处理器被称为事件处理器:
1): 文件事件处理器使用 I/O 多路复用来同时监听多个套接字,并根据套接字目前执行的任务为套接字关联不同的事件处理器.
2): 当被监听的套接字准备好 连接应答(accept),读取(read),写入(write),关闭(close)等操作时,与操作相对应的文件事件就会产生,此时就用之前关联好的事件处理器进行对应的处理.

所以,虽然 Redis 是一个单线程程序,但是通过 I/O复用技术 在保持 Redis 内部单线程设计简单性的前提下,保证了高性能的网络通信模型.

12.1.1 文件事件的处理器构成
        // 处理文件事件,阻塞时间由 tvp 决定
        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;

           /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            // 读事件
            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++;
        }

其中 rfileProc 和 wfileProc 都是函数指针

该段代码位于主循环中的时间处理函数中,可以看到是一个经典的 Reactor 的一个设计, aeApiPoll 作为一个中间层,屏蔽了下层的实现,获取当前活动的事件,通过一个遍历进行一个执行操作.

而 rfileProc 和 wfileProc 是通过 aeCreateFileEvent 进行处理器的一个指定.
在确定事件时,也同时确定处理器的指向,保留有更大的修改空间

12.1.2 I/O 多路复用程序的实现
/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

同过头文件声明来进行一个跨平台的实现,这也是一个经典做法.
该方法也是利用了编译规则来达到的.
include 本质就是将代码进行一个引入的过程,而函数的声明也是可以跟函数的定义分别进行一个编写,编译的时候,会先找到函数声明,再找到其定义,再进行一个链接,所以,当 include 的文件不同时,当然就会链接到不同的实现,从而实现跨平台的效果.

回到代码中, ae_evport ae_epoll.c ae_kqueue.c ae_select.c 分别由性能高低来进行一个头文件的优先级区分.

12.1.3 事件的类型

事件分为两大类,分别为可读事件和可写事件,
在可读事件中又可以分为客户端对服务器的 write 操作, close 操作, connect 操作, 套接字产生 AE_READABLE 事件
在可写事件中,套接字产生 AE_WRITABLE 事件.

12.1.4 API

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData)
将套接字进行一个监听行为加入 IO复用范围内,并将事件和事件处理器进行一个关联处理

void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
与一个 aeCreateFileEvent 是一个相反的函数, 进行一个从 Reactor 中移除,并取消事件和事件处理器的一个关联处理

int aeGetFileEvents(aeEventLoop *eventLoop, int fd)
获取被监听的事件类型

int aeWait(int fd, int mask, long long milliseconds)

阻塞 milliseconds 这么久的一个时间,等待并产生事件,时间通过 mask 传入

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
根据头文件 include 不同,有不同的实现,具体的行为是从 反应堆中 找出活跃的文件描述符.

int aeProcessEvents(aeEventLoop *eventLoop, int flags);
进行事件处理行为, aeApiPoll 的上层调用

char *aeGetApiName(void);
获取当前用的那种 IO复用库的名字.

12.1.5 文件事件的处理器
1). 连接应答处理器

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask);
当服务器进行初始化时,会将该函数与IO复用的监听套接字进行保定,如果监听套接字接受到客户端的 connect 函数的调用信息时,就执行该函数的内容

2): 命令请求处理器

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)
为 socket 中 read 的包装,
当客户端已经与服务器建立连接之后,再发送命令请求时,就会产生 AE_READABBLE 事件,从而调用该函数,进行后续的时间处理

3): 命令恢复处理器

void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask);
在执行请求处理器后,该处理器会将得到的名句进行一个回复给客户端的一个行为,当回复结束时,会进行一个处理器和 AE_WRITEABLE 的一个解绑行为.

4): 一次完成的客户端与服务器连接事件示例

服务器监听套接字绑定 AE_READABLE,当连入时,触发监听套接字的连接应答处理器,生成一个客户端对象,绑定 fd,加入IO复用,开通 AE_READABLE 事件,使得客户端可以和服务器进行一个对话,当客户端发送一次命令后,客户端对象将开通 AE_WRITEABLE 事件,服务器进行一个回复之后,再解除事件.

12.2 时间事件

Redis 的时间事件分为以下两类:
定时事件,在间隔一段事件后执行一次
周期性事件, 每间隔一段时间就执行一次


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;

时间事件结构如上.
是否为周期事件,取决于事件处理器是否返回了 AE_NOMORE 关键字.

书中的版本 Redis 只使用周期性事件,没有使用定时事件

12.2.1 实现

Redis 使用的链表进行一个定时事件的串联,每次循环从头到尾进行一个遍历.比较简单粗暴…

12.2.2 API

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)

创建一个定时事件

int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);

通过创建时给的 id 进行删除

static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop);

返回最近的一个时间事件

static int processTimeEvents(aeEventLoop *eventLoop)
时间事件的执行器,用来遍历所有的时间事件

12.2.3 时间事件应用实例 serverCron 函数
    /* Create the serverCron() time event, that's our main way to process
     * background operations. */
    // 为 serverCron() 创建时间事件
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }

可以看到 severCron 的启动就是通过时间事件运行的

12.3 事件的调度与执行
/*
 * 事件处理器的主循环
 */
void aeMain(aeEventLoop *eventLoop) {

    eventLoop->stop = 0;

    while (!eventLoop->stop) {

        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在主循环中进行 aeProcessEvents 的一个执行,直到服务器的结束.

需要注意的是,时间事件可能晚于预设的时间,在时间事件未到达时,可能执行了读写事件,占用了较长的时间,所以可能会出现不精准的情况

12.4 总结

Redis 是一个事件驱动程序, 事件分为 时间事件和文件事件 两类
文件事件处理器是通过 Rector 模式实现的网络通信程序
文件事件是对套接字操作的抽象,每次套接字变为 可应答,可写,可读时,对应的文件事件就会产生
文件事件总体上 分为 AE_READABLE 和 AE_WIRETEABLE 两类
时间事件分为定时事件和周期事件,定时时间只在指定的时间到达一次,周期就是间隔一点时间进行调用
服务器一般只执行 serverCron 周期事件
文件事件和时间事件是合作关系,服务器会进行一个轮流处理
时间事件的实际处理时间可能会更晚一点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值