Redis事件机制

分别转自:
https://segmentfault.com/a/1190000020014518?utm_source=tag-newest
https://cloud.tencent.com/developer/article/1054016

Redis 采用事件驱动机制来处理大量的网络IO。它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event。

Redis中的事件驱动库只关注网络IO,以及定时器。该事件库处理下面两类事件:

  • 文件事件(file event):用于处理 Redis 服务器和客户端之间的网络IO。
  • 时间事件(time eveat):Redis 服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的。
    事件驱动库的代码主要是在src/ae.c中实现的,其示意图如下所示。

clipboard.png

aeEventLoop是整个事件驱动的核心,它管理着文件事件表和时间事件列表,
不断地循环处理着就绪的文件事件和到期的时间事件。下面我们就先分别介绍文件事件和时间事件,然后讲述相关的aeEventLoop源码实现。

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。文件事件处理器使用IO多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。

Redis 使用的IO多路复用技术主要有:select、epoll、evport和kqueue等。每个IO多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae_select.c,ae_epoll.c, ae_kqueue.c等。Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该架构,比如 netty 和 libevent。

clipboard.png

如下图所示,文件事件处理器有四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器以及事件处理器。

clipboard.png

文件事件是对套接字操作的抽象,每当一个套接字准备好执行 accept、read、write和 close 等操作时,就会产生一个文件事件。因为 Redis 通常会连接多个套接字,所以多个文件事件有可能并发的出现。

I/O多路复用程序负责监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字。

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的套接字都放到同一个队列(也就是后文中描述的aeEventLoop的fired就绪事件表)里边,然后文件事件处理器会以有序、同步、单个套接字的方式处理该队列中的套接字,也就是处理就绪的文件事件。

clipboard.png

所以,一次 Redis 客户端与服务器进行连接并且发送命令的过程如上图所示。

客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。
客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。
执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中。

时间事件

Redis 的时间事件分为以下两类:

  • 定时事件:让一段程序在指定的时间之后执行一次。
  • 周期性事件:让一段程序每隔指定时间就执行一次。

Redis 的时间事件的具体定义结构如下所示。

typedef struct aeTimeEvent {
    /* 全局唯一ID */
    long long id; /* time event identifier. */
    /* 秒精确的UNIX时间戳,记录时间事件到达的时间*/
    long when_sec; /* seconds */
    /* 毫秒精确的UNIX时间戳,记录时间事件到达的时间*/
    long when_ms; /* milliseconds */
    /* 时间处理器 */
    aeTimeProc *timeProc;
    /* 事件结束回调函数,析构一些资源*/
    aeEventFinalizerProc *finalizerProc;
    /* 私有数据 */
    void *clientData;
    /* 前驱节点 */
    struct aeTimeEvent *prev;
    /* 后继节点 */
    struct aeTimeEvent *next;
} aeTimeEvent;

一个时间事件是定时事件还是周期性事件取决于时间处理器的返回值:

  • 如果返回值是 AE_NOMORE,那么这个事件是一个定时事件,该事件在达到后删除,之后不会再重复。
  • 如果返回值是非 AE_NOMORE
    的值,那么这个事件为周期性事件,当一个时间事件到达后,服务器会根据时间处理器的返回值,对时间事件的 when
    属性进行更新,让这个事件在一段时间后再次达到。 Redis 将所有时间事件都放在一个无序链表中,每次 Redis
    会遍历整个链表,查找所有已经到达的时间事件,并且调用相应的事件处理器。

介绍完文件事件和时间事件,我们接下来看一下 aeEventLoop的具体实现。

创建事件管理器

Redis 服务端在其初始化函数 initServer中,会创建事件管理器aeEventLoop对象。

函数aeCreateEventLoop将创建一个事件管理器,主要是初始化 aeEventLoop的各个属性值,比如events、fired、timeEventHead和apidata:

  • 首先创建aeEventLoop对象。
  • 初始化未就绪文件事件表、就绪文件事件表。events指针指向未就绪文件事件表、fired指针指向就绪文件事件表。表的内容在后面添加具体事件时进行初变更。
  • 初始化时间事件列表,设置timeEventHead和timeEventNextId属性。 调用aeApiCreate
  • 函数创建epoll实例,并初始化 apidata。
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    int i;
    /* 创建事件状态结构 */
    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
    /* 创建未就绪事件表、就绪事件表 */
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
    if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
    /* 设置数组大小 */
    eventLoop->setsize = setsize;
    /* 初始化执行最近一次执行时间 */
    eventLoop->lastTime = time(NULL);
    /* 初始化时间事件结构 */
    eventLoop->timeEventHead = NULL;
    eventLoop->timeEventNextId = 0;
    eventLoop->stop = 0;
    eventLoop->maxfd = -1;
    eventLoop->beforesleep = NULL;
    eventLoop->aftersleep = NULL;
    /* 将多路复用io与事件管理器关联起来 */
    if (aeApiCreate(eventLoop) == -1) goto err;
    /* 初始化监听事件 */
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;
    return eventLoop;
err:
   .....
}

aeApiCreate 函数首先创建了aeApiState对象,初始化了epoll就绪事件表;然后调用epoll_create创建了epoll实例,最后将该aeApiState赋值给apidata属性。

aeApiState对象中epfd存储epoll的标识,events是一个epoll就绪事件数组,当有epoll事件发生时,所有发生的epoll事件和其描述符将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。

static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;
    /* 初始化epoll就绪事件表 */
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }
    /* 创建 epoll 实例 */
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }
    /* 事件管理器与epoll关联 */
    eventLoop->apidata = state;
    return 0;
}
typedef struct aeApiState {
    /* epoll_event 实例描述符*/
    int epfd;
    /* 存储epoll就绪事件表 */
    struct epoll_event *events;
} aeApiState;

创建文件事件

aeFileEvent是文件事件结构,对于每一个具体的事件,都有读处理函数和写处理函数等。Redis 调用aeCreateFileEvent函数针对不同的套接字的读写事件注册对应的文件事件。

typedef struct aeFileEvent {
    /* 监听事件类型掩码,值可以是 AE_READABLE 或 AE_WRITABLE */
    int mask;
    /* 读事件处理器 */
    aeFileProc *rfileProc;
    /* 写事件处理器 */
    aeFileProc *wfileProc;
    /* 多路复用库的私有数据 */
    void *clientData;
} aeFileEvent;
/* 使用typedef定义的处理器函数的函数类型 */
typedef void aeFileProc(struct aeEventLoop *eventLoop, 
int fd, void *clientData, int mask);

比如说,Redis 进行主从复制时,从服务器需要主服务器建立连接,它会发起一个 socekt连接,然后调用aeCreateFileEvent函数针对发起的socket的读写事件注册了对应的事件处理器,也就是syncWithMaster函数。

aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL);
/* 符合aeFileProc的函数定义 */
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {....}

aeCreateFileEvent的参数fd指的是具体的socket套接字,proc指fd产生事件时,具体的处理函数,clientData则是回调处理函数时需要传入的数据。
aeCreateFileEvent主要做了三件事情:

  • 以fd为索引,在events未就绪事件表中找到对应事件。
  • 调用aeApiAddEvent函数,该事件注册到具体的底层 I/O 多路复用中,本例为epoll。
  • 填充事件的回调、参数、事件类型等参数。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
                       aeFileProc *proc, void *clientData)
{
    /* 取出 fd 对应的文件事件结构, fd 代表具体的 socket 套接字 */
    aeFileEvent *fe = &eventLoop->events[fd];
    /* 监听指定 fd 的指定事件 */
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    /* 置文件事件类型,以及事件的处理器 */
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    /* 私有数据 */
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

如上文所说,Redis 基于的底层 I/O 多路复用库有多套,所以aeApiAddEvent也有多套实现,下面的源码是epoll下的实现。其核心操作就是调用epoll的epoll_ctl函数来向epoll注册响应事件。有关epoll相关的知识可以看一下《Java NIO源码分析》

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    /* 如果 fd 没有关联任何事件,那么这是一个 ADD 操作。如果已经关联了某个/某些事件,那么这是一个 MOD 操作。 */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    /* 注册事件到 epoll */
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    /* 调用epoll_ctl 系统调用,将事件加入epoll中 */
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

事件处理

因为 Redis 中同时存在文件事件和时间事件两个事件类型,所以服务器必须对这两个事件进行调度,决定何时处理文件事件,何时处理时间事件,以及如何调度它们。

aeMain函数以一个无限循环不断地调用aeProcessEvents函数来处理所有的事件。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        /* 如果有需要在事件处理前执行的函数,那么执行它 */
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        /* 开始处理事件*/
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

下面是aeProcessEvents的伪代码,它会首先计算距离当前时间最近的时间事件,以此计算一个超时时间;然后调用aeApiPoll函数去等待底层的I/O多路复用事件就绪;aeApiPoll函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。

/* 伪代码 */
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    /* 获取到达时间距离当前时间最接近的时间事件*/
    time_event = aeSearchNearestTimer();
    /* 计算最接近的时间事件距离到达还有多少毫秒*/
    remaind_ms = time_event.when - unix_ts_now();
    /* 如果事件已经到达,那么remaind_ms为负数,将其设置为0 */
    if (remaind_ms < 0) remaind_ms = 0;
    /* 根据 remaind_ms 的值,创建 timeval 结构*/
    timeval = create_timeval_with_ms(remaind_ms);
    /* 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 结构决定,如果remaind_ms 的值为0,则aeApiPoll 调用后立刻返回,不阻塞*/
    /* aeApiPoll调用epoll_wait函数,等待I/O事件*/
    aeApiPoll(timeval);
    /* 处理所有已经产生的文件事件*/
    processFileEvents();
    /* 处理所有已经到达的时间事件*/
    processTimeEvents();
}

与aeApiAddEvent类似,aeApiPoll也有多套实现,它其实就做了两件事情,调用epoll_wait阻塞等待epoll的事件就绪,超时时间就是之前根据最快达到时间事件计算而来的超时时间;然后将就绪的epoll事件转换到fired就绪事件。aeApiPoll就是上文所说的I/O多路复用程序。具体过程如下图所示。

clipboard.png

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) 
{
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    // 调用epoll_wait函数,等待时间为最近达到时间事件的时间计算而来。
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    // 有至少一个事件就绪?
    if (retval > 0) 
    {
        int j;
        /*为已就绪事件设置相应的模式,并加入到 eventLoop 的 fired 数组中*/
        numevents = retval;
        for (j = 0; j < numevents; j++) 
    {
            int mask = 0;
            struct epoll_event *e = state->events+j;
            if (e->events & EPOLLIN)
        mask |= AE_READABLE;
            if (e->events & EPOLLOUT)
        mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) 
        mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP)
        mask |= AE_WRITABLE;
            /* 设置就绪事件表元素 */
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    
    // 返回已就绪事件个数
    return numevents;
}

processFileEvent是处理就绪文件事件的伪代码,也是上文所述的文件事件分派器,它其实就是遍历fired就绪事件表,然后根据对应的事件类型来调用事件中注册的不同处理器,读事件调用rfileProc,而写事件调用wfileProc。

void processFileEvent(int numevents) {
    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 fired = 0;
            int invert = fe->mask & AE_BARRIER;
            /* 读事件 */
            if (!invert && fe->mask & mask & AE_READABLE) {
                /* 调用读处理函数 */
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }
            /* 写事件. */
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            if (invert && fe->mask & mask & AE_READABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            processed++;
        }
    }
}

而processTimeEvents是处理时间事件的函数,它会遍历aeEventLoop的事件事件列表,如果时间事件到达就执行其timeProc函数,并根据函数的返回值是否等于AE_NOMORE来决定该时间事件是否是周期性事件,并修改器到达时间。

static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);
    ....
    eventLoop->lastTime = now;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    /* 遍历时间事件链表 */
    while(te) {
        long now_sec, now_ms;
        long long id;

        /* 删除需要删除的时间事件 */
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            te = next;
            continue;
        }

        /* id 大于最大maxId,是该循环周期生成的时间事件,不处理 */
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        aeGetTime(&now_sec, &now_ms);
        /* 事件已经到达,调用其timeProc函数*/
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;

            id = te->id;
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            /* 如果返回值不等于 AE_NOMORE,表示是一个周期性事件,修改其when_sec和when_ms属性*/
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                /* 一次性事件,标记为需删除,下次遍历时会删除*/
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}

删除事件

当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。

aeDeleteEventLoop函数的执行过程总结为以下几个步骤
1、根据fd在未就绪表中查找到事件
2、取消该fd对应的相应事件标识符
3、调用aeApiFree函数,内核会将epoll监听红黑树上的相应事件监听取消。

另一篇纯概念的文章:

一、时间事件

1、概述

     redis的时间事件分为两类,一类是定时事件,在某个时刻执行;另一类是周期性事件,每隔一段时间执行一次。

时间事件由三部分组成——全局id,标识事件,新的事件比旧的事件id大;unix毫秒级时间戳,记录时间事件的到达时间;事件处理器,时间事件到达时调用相应的处理器进行处理。

一个时间事件是定时还是周期性,取决于其返回值:如果返回的是AE_NOMORE,表示其是一次性的事件,即定时的,执行完毕后redis会将其删除;如果返回的不是该结果,则表示是周期性事件,服务器会根据返回值,更新unix毫秒级时间戳,这样当下一次时间到达时又会执行该事件。

2、实现

redis将所有的时间事件放在一个无序链表中,当时间事件执行器执行时,会遍历整个链表,将所有已到达执行时间的事件调用相应的时间事件处理器进行处理。

虽然是无序链表,但是由于新的时间事件总是插入到表头,因此表头总是最新的时间事件。

无序链表结构如下图:
在这里插入图片描述

采用无序链表,是指链表的元素不按照when的先后进行排序,而是强制遍历整个链表,确保所有到达执行时间的事件都能被执行。

在目前的情况下,redis只使用serverCron函数作为时间事件,相当于仅有一个时间事件,因此这个链表可以简化为一个指针。

流程如下:

     1)遍历服务器中所有的时间事件。

     2)检查每个事件是否到达执行时间,对于已经到达的,执行事件,并获取返回值。

     3)如果返回的是AE_NOMORE,表示是定时事件,则服务器删除该事件;否则表示其是周期性事件,服务器会更新事件的when属性,标记下一次执行时间。

3、serverCron函数

     redis目前仅使用serverCron函数作为时间事件,即目前仅有这一个时间事件。该时间事件主要进行以下操作:

     1)更新redis服务器各类统计信息,包括时间、内存占用、数据库占用等情况。

     2)清理数据库中的过期键值对。

     3)关闭和清理连接失败的客户端。

     4)尝试进行aof和rdb持久化操作。

     5)如果服务器是主服务器,会定期将数据向从服务器做同步操作。

     6)如果处于集群模式,对集群定期进行同步与连接测试操作。

     redis服务器开启后,就会周期性执行此函数,直到redis服务器关闭为止。默认每秒执行10次,平均100毫秒执行一次,可以在redis配置文件的hz选项,调整该函数每秒执行的次数。

二、事件调度与执行

由于redis服务器同时存在文件事件和时间事件,因此必须对这两个事件进行调度,决定何时处理文件事件,何时处理时间事件,以及花费多少时间处理这两类事件。

1、事件调度执行流程

事件的调度和执行由ae.c/aeProcessEvents函数负责,执行流程如下:
在这里插入图片描述

     1)启动服务器,初始化服务器,一直处理事件,循环下面的2~6步骤,直到服务器关闭。服务器关闭会执行相关的清理操作。

     2)获取到达时间距离当前时间最近的时间事件,计算到达时间,单位是毫秒,假设结果是x毫秒。

     3)如果时间已经到达,则x是负数,将x置成0。

     4)如果x不是0,则程序阻塞,等待文件事件的产生,再进入下一步,其中程序阻塞的最大等待时间是x毫秒,因为即使x毫秒内都没有文件事件的产生,但是x毫秒后必然有时间事件需要执行,因此不能继续阻塞;如果x是0,即已经有时间事件到了需要执行的时候,则程序不阻塞,直接进入下一步。

     5)处理所有已经产生的文件事件。

     6)处理所有已经产生的时间事件。

2、事件调度执行规则

     1)程序等待文件事件的最大阻塞时间,是由到达时间最接近当前时间的时间时间决定,即避免了程序对时间事件的不断轮询,又保证阻塞时间不会太长。

     2)由于文件事件的发生,是由客户端决定,即完全随机的。因此程序处理完一个文件事件后,如果没有新的待处理的文件事件,且还没到达最近的时间事件的执行时间,则程序会继续阻塞,直到达到最近的时间事件的时间,或期间有新的文件事件。

     3)文件事件和时间事件都是同步、有序、原子的执行,执行一个事件的时候,其他事件会阻塞等待,不会发生事件的抢占。两类事件处理器都会减少程序的阻塞时间,并在有需要的时候主动让出执行权,避免事件的饥饿等待。

例如:文件事件的命令回复处理器,如果内容太多,写入的字节数超出预设的常量,则处理器会自动break,留下剩余的内容下一次再写;时间事件中会将耗时的rdb持久化、aof重写等操作,通过创建子进程,由子进程执行。

     4)由于都是优先处理文件事件,后处理时间事件,且处理过程不会发生事件抢占,因此时间事件的实际执行时间,有可能比设定的执行时间稍晚一些。

执行示例如下图:
在这里插入图片描述

在等待下一个时间事件的过程中,程序处理了两个文件事件。其中第85毫秒,由于还没到时间事件的执行时间,而有文件事件,因此处理文件事件。由于文件事件处理完毕后是在130毫秒,则时间事件只能在131毫秒执行,比预设的100毫秒晚了31毫秒。

三、总结

1、redis服务器是事件驱动程序,事件分为文件事件和时间事件。

2、文件事件处理程序是基于Reactor的网络通信程序,是对套接字操作的抽象,每当套接字变成可应答、可读、可写的时候,相应的文件事件就会产生。

3、文件事件分为读事件(AE_READABLE)和写事件(AE_WRITEABLE)两类。

4、时间事件分为定时事件和周期性事件,当前redis只有周期性事件,且周期性事件中只有一个事件——serverCron函数,该函数默认每秒执行10次,可以通过配置文件修改每秒执行次数。

5、文件事件和时间事件是合作关系,服务器会轮流处理两个事件,事件不会发生抢占。由于通常先处理文件事件,因此时间事件的实际执行时间有可能比预设时间稍晚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值