zoukankan      html  css  js  c++  java
  • 探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程

    本文转自互联网

    本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看

    https://github.com/h2pl/Java-Tutorial

    喜欢的话麻烦点下Star哈

    文章首发于我的个人博客:

    www.how2playlife.com

    本文是微信公众号【Java技术江湖】的《探索Redis设计与实现》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。

    该系列博文会告诉你如何从入门到进阶,Redis基本的使用方法,Redis的基本数据结构,以及一些进阶的使用方法,同时也需要进一步了解Redis的底层数据结构,再接着,还会带来Redis主从复制、集群、分布式锁等方面的相关内容,以及作为缓存的一些使用方法和注意事项,以便让你更完整地了解整个Redis相关的技术体系,形成自己的知识框架。

    如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

    原文地址:https://www.xilidou.com/2018/03/22/redis-event/

    Redis 是一个事件驱动的内存数据库,服务器需要处理两种类型的事件。

    文件事件
    时间事件
    下面就会介绍这两种事件的实现原理。

    文件事件
    Redis 服务器通过 socket 实现与客户端(或其他redis服务器)的交互,文件事件就是服务器对 socket 操作的抽象。 Redis 服务器,通过监听这些 socket 产生的文件事件并处理这些事件,实现对客户端调用的响应。

    Reactor
    Redis 基于 Reactor 模式开发了自己的事件处理器。

    这里就先展开讲一讲 Reactor 模式。看下图:
    image
    reactor

    “I/O 多路复用模块”会监听多个 FD ,当这些FD产生,accept,read,write 或 close 的文件事件。会向“文件事件分发器(dispatcher)”传送事件。

    文件事件分发器(dispatcher)在收到事件之后,会根据事件的类型将事件分发给对应的 handler。

    我们顺着图,从上到下的逐一讲解 Redis 是怎么实现这个 Reactor 模型的。

    I/O 多路复用模块
    Redis 的 I/O 多路复用模块,其实是封装了操作系统提供的 select,epoll,avport 和 kqueue 这些基础函数。向上层提供了一个统一的接口,屏蔽了底层实现的细节。

    一般而言 Redis 都是部署到 Linux 系统上,所以我们就看看使用 Redis 是怎么利用 linux 提供的 epoll 实现I/O 多路复用。

    首先看看 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);
    再看 Redis 对文件事件,封装epoll向上提供的接口:
    
    
    /*
     * 事件状态
     */
    typedef struct aeApiState {
    
        // epoll_event 实例描述符
        int epfd;
    
        // 事件槽
        struct epoll_event *events;
    
    } aeApiState;
    
    /*
     * 创建一个新的 epoll 
     */
    static int  aeApiCreate(aeEventLoop *eventLoop)
    /*
     * 调整事件槽的大小
     */
    static int  aeApiResize(aeEventLoop *eventLoop, int setsize)
    /*
     * 释放 epoll 实例和事件槽
     */
    static void aeApiFree(aeEventLoop *eventLoop)
    /*
     * 关联给定事件到 fd
     */
    static int  aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
    /*
     * 从 fd 中删除给定事件
     */
    static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)
    /*
     * 获取可执行事件
     */
    static int  aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
    

    所以看看这个ae_peoll.c 如何对 epoll 进行封装的:

    aeApiCreate() 是对 epoll.epoll_create() 的封装。
    aeApiAddEvent()和aeApiDelEvent() 是对 epoll.epoll_ctl()的封装。
    aeApiPoll() 是对 epoll_wait()的封装。
    这样 Redis 的利用 epoll 实现的 I/O 复用器就比较清晰了。

    再往上一层次我们需要看看 ea.c 是怎么封装的?

    首先需要关注的是事件处理器的数据结构:

    typedef struct aeFileEvent {
    
        // 监听事件类型掩码,
        // 值可以是 AE_READABLE 或 AE_WRITABLE ,
        // 或者 AE_READABLE | AE_WRITABLE
        int mask; /* one of AE_(READABLE|WRITABLE) */
    
        // 读事件处理器
        aeFileProc *rfileProc;
    
        // 写事件处理器
        aeFileProc *wfileProc;
    
        // 多路复用库的私有数据
        void *clientData;
    
    } aeFileEvent;
    

    mask 就是可以理解为事件的类型。

    除了使用 ae_peoll.c 提供的方法外,ae.c 还增加 “增删查” 的几个 API。

    增:aeCreateFileEvent
    删:aeDeleteFileEvent
    查: 查包括两个维度 aeGetFileEvents 获取某个 fd 的监听类型和aeWait等待某个fd 直到超时或者达到某个状态。
    事件分发器(dispatcher)
    Redis 的事件分发器 ae.c/aeProcessEvents 不但处理文件事件还处理时间事件,所以这里只贴与文件分发相关的出部分代码,dispather 根据 mask 调用不同的事件处理器。

    //从 epoll 中获关注的事件
    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;
    
        // 读事件
        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++;
    }
    

    可以看到这个分发器,根据 mask 的不同将事件分别分发给了读事件和写事件。

    文件事件处理器的类型
    Redis 有大量的事件处理器类型,我们就讲解处理一个简单命令涉及到的三个处理器:

    acceptTcpHandler 连接应答处理器,负责处理连接相关的事件,当有client 连接到Redis的时候们就会产生 AE_READABLE 事件。引发它执行。
    readQueryFromClinet 命令请求处理器,负责读取通过 sokect 发送来的命令。
    sendReplyToClient 命令回复处理器,当Redis处理完命令,就会产生 AE_WRITEABLE 事件,将数据回复给 client。
    文件事件实现总结
    我们按照开始给出的 Reactor 模型,从上到下讲解了文件事件处理器的实现,下面将会介绍时间时间的实现。

    时间事件
    Reids 有很多操作需要在给定的时间点进行处理,时间事件就是对这类定时任务的抽象。

    先看时间事件的数据结构:

    /* Time event structure
     *
     * 时间事件结构
     */
    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;
    

    看见 next 我们就知道这个 aeTimeEvent 是一个链表结构。看图:
    image

    timeEvent

    注意这是一个按照id倒序排列的链表,并没有按照事件顺序排序。

    processTimeEvent
    Redis 使用这个函数处理所有的时间事件,我们整理一下执行思路:

    记录最新一次执行这个函数的时间,用于处理系统时间被修改产生的问题。
    遍历链表找出所有 when_sec 和 when_ms 小于现在时间的事件。
    执行事件对应的处理函数。
    检查事件类型,如果是周期事件则刷新该事件下一次的执行事件。
    否则从列表中删除事件。
    综合调度器(aeProcessEvents)
    综合调度器是 Redis 统一处理所有事件的地方。我们梳理一下这个函数的简单逻辑:

    // 1. 获取离当前时间最近的时间事件
    shortest = aeSearchNearestTimer(eventLoop);
    
    // 2. 获取间隔时间
    timeval = shortest - nowTime;
    
    // 如果timeval 小于 0,说明已经有需要执行的时间事件了。
    if(timeval < 0){
        timeval = 0
    }
    
    // 3. 在 timeval 时间内,取出文件事件。
    numevents = aeApiPoll(eventLoop, timeval);
    
    // 4.根据文件事件的类型指定不同的文件处理器
    if (AE_READABLE) {
        // 读事件
        rfileProc(eventLoop,fd,fe->clientData,mask);
    }
        // 写事件
    if (AE_WRITABLE) {
        wfileProc(eventLoop,fd,fe->clientData,mask);
    }
    

    以上的伪代码就是整个 Redis 事件处理器的逻辑。

    我们可以再看看谁执行了这个 aeProcessEvents:

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

    然后我们再看看是谁调用了 eaMain:

    int main(int argc, char **argv) {
        //一些配置和准备
        ...
        aeMain(server.el);
        
        //结束后的回收工作
        ...
    }
    

    我们在 Redis 的 main 方法中找个了它。

    这个时候我们整理出的思路就是:

    Redis 的 main() 方法执行了一些配置和准备以后就调用 eaMain() 方法。

    eaMain() while(true) 的调用 aeProcessEvents()。

    所以我们说 Redis 是一个事件驱动的程序,期间我们发现,Redis 没有 fork 过任何线程。所以也可以说 Redis 是一个基于事件驱动的单线程应用。

    总结
    在后端的面试中 Redis 总是一个或多或少会问到的问题。

    读完这篇文章你也许就能回答这几个问题:

    为什么 Redis 是一个单线程应用?
    为什么 Redis 是一个单线程应用,却有如此高的性能?
    如果你用本文提供的知识点回答这两个问题,一定会在面试官心中留下一个高大的形象。

    大家还可以阅读我的 Redis 相关的文章:

  • 相关阅读:
    自定义View的ToolBar布局报错Error:(2) No resource identifier found for attribute 'context' in package 'c
    在学git之主分支 branch
    获取发布版SHA1
    关于开启线程与UI的操作
    播放音频和视频(VideoView控件)
    通知栏Notification的应用
    Android 真机调式 Installation failed with message 远程主机强迫关闭了一个现有的连接。. It is possible that this issue is resolved by uninstalling an existing version of the apk if it is present, and then re-installing. WA
    运行程序申请危险权限
    mysql乐观锁总结和实践
    Nginx配置文件nginx.conf中文详解
  • 原文地址:https://www.cnblogs.com/AliCoder/p/11876697.html
Copyright © 2011-2022 走看看