Redis源码分析笔记5-事件处理组件AE

简介:

本文主要介绍redis事件处理组件。行文顺序:

  1. redis事件处理组件的基本概念及源码文件组成;
  2. redis事件组件程序框架;
  3. redis事件组件程序整体流程;

基本概念:

redis事件分为文件事件和时间事件两类:

  • 文件事件(fileevent):
    redis服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端或者服务器与其它服务器的通信会产生相应的文件是按。而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件(timeevent):
    redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行。而时间事件就是服务器对这类定时操作的抽象。
    ————————————(引用自《redis设计与实现》)

事件处理器程序框架

这部分需要稍微费一些笔墨,我从文件事件和时间事件两个方面分析。

redis模型采用Reactor设计模式设计。有关Reactor设计模式的内容大家可以参考:
http://www.cnblogs.com/hzbook/archive/2012/07/19/2599698.html

文件事件:
文件事件的处理器:
redis为文件事件编写了多个处理器,这些时间处理器分别用于实现不同过得网络通信需求,比如说:
为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器;
为了接受客户单传来的命令请求,服务器要为客户端套接字关联命令请求处理器;
为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器;
当主服务器和从服务器进行复制操作时,主从服务器都需要关联特别为复制功能编写的复制处理器。
我们看一下文件事件处理器的四个组成部分:
这里写图片描述

虽然文件事件处理器以单前程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与redis服务器中同样以单线程方式运行的模块进行对接,这保持了redis内部单线程设计的简单性。

redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。每个I/O多路复用对应着一个单独的文件。
在ae.c中用INCLUDE宏来定义了相应的规则。

/* 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
//GD bgl-pub2:/home/d5000/kedong/src/platform/personal_test/liyang/redis-3.0-annotated-unstable/src % grep -r HAVE_EPOLL *
ae.c:52:    #ifdef HAVE_EPOLL
config.h:65:#define HAVE_EPOLL 1

发现在config.h中做了条件编译,那么看一下config.h:65干了什么?

 63 /* Test for polling API */
 64 #ifdef __linux__
 65 #define HAVE_EPOLL 1
 66 #endif

ok看到是在查找系统环境变量,我想看一下我的redis环境用的是哪种I/O多路复用模型
在src/Makefile中未定义linux,确定原来我编译的redis是用的select模型。
这种实现方法是不是很巧妙啊。

事件连接处理器:

/*
 * 创建一个 TCP 连接处理器
 */
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);
    while(max--) {
        // accept 客户端连接
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        // 为客户端创建客户端状态(redisClient)
        acceptCommonHandler(cfd,0);
    }
}

主要流程如下所示:
这里写图片描述

命令请求处理器:

/*
 * 读取客户端的查询缓冲区内容
 */
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
    size_t qblen;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    // 设置服务器的当前客户端
    server.current_client = c;

    // 读入长度(默认为 16 MB)
    readlen = REDIS_IOBUF_LEN;
    /* If this is a multi bulk request, and we are processing a bulk reply
     * that is large enough, try to maximize the probability that the query
     * buffer contains exactly the SDS string representing the object, even
     * at the risk of requiring more read(2) calls. This way the function
     * processMultiBulkBuffer() can avoid copying buffers to create the
     * Redis Object representing the argument. */
    if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= REDIS_MBULK_BIG_ARG)
    {
        int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
        if (remaining < readlen) readlen = remaining;
    }
    // 获取查询缓冲区当前内容的长度
    // 如果读取出现 short read ,那么可能会有内容滞留在读取缓冲区里面
    // 这些滞留内容也许不能完整构成一个符合协议的命令,
    qblen = sdslen(c->querybuf);
    // 如果有需要,更新缓冲区内容长度的峰值(peak)
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    // 为查询缓冲区分配空间
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    // 读入内容到查询缓存
    nread = read(fd, c->querybuf+qblen, readlen);
    // 读入出错
    if (nread == -1) {
        if (errno == EAGAIN) {
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    // 遇到 EOF
    } else if (nread == 0) {
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }
    if (nread) {
        // 根据内容,更新查询缓冲区(SDS) free 和 len 属性
        // 并将 '\0' 正确地放到内容的最后
        sdsIncrLen(c->querybuf,nread);
        // 记录服务器和客户端最后一次互动的时间
        c->lastinteraction = server.unixtime;
        // 如果客户端是 master 的话,更新它的复制偏移量
        if (c->flags & REDIS_MASTER) c->reploff += nread;
    } else {
        // 在 nread == -1 且 errno == EAGAIN 时运行
        server.current_client = NULL;
        return;
    }
    // 查询缓冲区长度超出服务器最大缓冲区长度
    // 清空缓冲区并释放客户端
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
        bytes = sdscatrepr(bytes,c->querybuf,64);
        redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }
    // 从查询缓存重读取内容,创建参数,并执行命令
    // 函数会执行到缓存中的所有内容都被处理完为止
    processInputBuffer(c);
    server.current_client = NULL;
}

命令回复处理器:

文件事件处理器组件框架图:
一个完整调用:
step1服务端监听客户端连接请求
step2客户端请求与服务端建立连接
step3客户端发送命令请求到服务端
step4客户端读取服务端返回客户端命令执行结果
整个过程如下图所示:
这里写图片描述


文件事件API:
aeCreateFileEvent将给定套接字的给定事件加入到I/O多路复用程序的监听范围之内,并对时间和时间处理器进行关联。

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData);

aeDeleteFileEvent**取消给定套接字的给定事件加入到I/O多路复用程序的监听范围之内,并取消**对时间和时间处理器进行关联。

void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);

aeGetFileEvents接受一个套接字描述符,返回该套接字正在被监听的事件类型:

int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
int aeWait(int fd, int mask, long long milliseconds);
void aeMain(aeEventLoop *eventLoop);
char *aeGetApiName(void);


时间事件:
redis的时间时间分为以下两类:
定时事件:让一段程序在指定的时间之后执行一次。比如让程序X在当前时间之后30毫秒执行一次。
周期性事件:让一段程序每隔指定时间就执行一次。比如让程序Y每隔30秒就执行一次。

用引自《redis设计与实现》一书中的一段描述解释
这里写图片描述
目前版本的redis只使用周期事件,而没有使用定时事件。

/*
 * 创建时间事件
 */
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    // 更新时间计数器
    long long id = eventLoop->timeEventNextId++;
    // 创建时间事件结构
    aeTimeEvent *te;
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    // 设置 ID
    te->id = id;
    // 设定处理事件的时间
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    // 设置事件处理器
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    // 设置私有数据
    te->clientData = clientData;
    // 将新事件放入表头
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

redis事件处理组件完整流程图:
这里写图片描述

下图表示以下三个流程:

  • 客户端与服务端建立连接;
  • 客户端向服务端发送命令;
  • 服务端回复客户端命令。
    这里写图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值