范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Redis为什么这么快?

  文章转载自腾讯云数据库
  | 作者 吴显坚,腾讯云数据库高级工程师,参与过360开源项目Pika的研发工作,现从事redis数据库研发工作。
  Redis服务器是一个事件驱动程序, 事件是Redis服务器的核心, 它处理两项重要的任务, 一个是IO事件(文件事件), 另外一个是时间事件. Redis服务器通过套接字与客户端进行连接, 而文件事件可以理解为服务器对套接字操作的抽象. 服务器与客户端的通信会产生相应的文件事件, 而服务器则通过监听并处理这些事件来完成一系列网络通信操作. 另外Redis内部有一些操作(从Redis4.0的代码分析目前时间事件只有serverCron)需要在给定的时间点执行, 而时间事件就是Redis服务器对这类定时操作的抽象。
  一、aeEventLoop
  在分析具体代码之前, 我们先了解一下在事件处理中处于核心部分的aeEventLoop到底是什么:/* State of an event based program */typedef struct aeEventLoop {    intmaxfd;                 /* highest filedescriptor currently registered, 当前注册的最大fd数 */    intsetsize;               /* max number offile descriptors tracked, 允许关注fd数量的上限(就是events和fired数组的大小) */    longlong timeEventNextId  /* 下一个定时事件的ID */;   time_t lastTime;           /* Usedto detect system clock skew */   aeFileEvent *events;       /*Registered events, 指向aeFileEvent数组, 每个fd都有对应的aeFileEvent对象(fd就是当做下标用于索引)*/   aeFiredEvent *fired;       /*Fired events, 指向aeFiredEvent数组,当检测到有文件事件时, 会将对应的fd和事件存放在数组里面*/   aeTimeEvent *timeEventHead /* 指向首个时间事件结构体, 而时间事件结构体里有next指针, 指向下一个结构体, 实际上整体看上去是一个环形链表(最后一个时间事件结构体里的next指针会指向timeEventHead */;    intstop                   /* 是否停止event loop循环 */;    void*apidata;             /* This is used forpolling API specific data, 指向底层不同多路复用实现的数据结构, 可以是epoll, select, evport或者是kqueue */   aeBeforeSleepProc *beforesleep;   aeBeforeSleepProc *aftersleep;} aeEventLoop;
  创建aeEventLoop只需要一个setsize参数, 它标识了当前aeEventLoop最大可以监听的文件描述符数(通常redis传入server.maxclients+CONFIG_FDSET_INCR,也就是在用户指定的最大客户端连接数的基础上再额外增加128, 这128可以用于Redis内部打开AOF,RDB文件以及主从, 集群互相通信所对应的文件句柄), 创建aeEventLoop时, aeFileEvent和aeFiredEvent数组的大小就由setsize确定。
  1. aeFileEvent
  内部以掩码的形式存储了当前套接字关心的事件(可读/可写事件), 内部还有两个函数指针指向可读/可写事件发生时应该调用的函数, 另外还有一个无类型的指针指向相关联的数据, 这里需要注意的是, events是一个数组, 而套接字就是作为下标来进行索引对应aeFileEvent, 例如我当前关心的套接字是9, 那么events[9]就是它对应的文件事件数据结构(csapp中提到过, 当我们调用系统函数返回描述符数字时, 返回的描述符总是在进程中当前没有打开的最小描述符, 所以我们无需担心文件描述符被反复的创建销毁, 而越来越大的问题)。
  2. aeFiredEvent
  内部以掩码的形式存储了当前已经触发的事件和对应的套接字, 实际上fired数组只有在调用aeApiPoll的时候才会被赋值, 例如当前发现有套接字6, 8有可读事件, 而套接字10有可写事件, 那么fired数组的前三个元素会被赋值{fd = 6, mask =AE_READABLE}, {fd = 8, mask = AE_READABLE}, {fd = 10, mask = AE_WRITABLE}, 紧接着我们以6为索引, 找到文件事件数据结构events[6],然后发现触发的是可读事件, 我们再调用events[6]中rfileProc来处理可读事件。
  aeEventLoop *aeCreateEventLoop(int setsize){   aeEventLoop *eventLoop;    inti;     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;    if(aeApiCreate(eventLoop) == -1) goto err;    /*Events with mask == AE_NONE are not set. So let"s initialize the    * vector with it. */    for(i = 0; i < setsize; i++)       eventLoop->events[i].mask = AE_NONE;    returneventLoop;}
  对于时间事件, aeEventLoop中有一个timeEventHead指针指向第一个时间事件, 由于aeEventLoop创建之初, 内部没有任何时间事件, 所以初始化时timeEventHead指向NULL, 每当有新的时间事件时, 总会被添加到timeEventHead头部, 由于aeTimeEvent结构体中有next指针可以指向下一个aeTimeEvent结构体, 所以只要我们获取timeEventHead就能遍历当前所有的时间事件了, 另外有一个细节需要注意, 最后一个aeTimeEvent结构体中的next指针指向的是timeEventHead, 所以所有时间事件实际上就是由一个环形链表串连起来的。
  二、文件事件
  在介绍中有提到过文件事件实际上就是服务器对套接字操作的抽象, 当套接字有可读写事件触发的时候, 我们需要调用相应的处理函数, 下面先看一下跟文件事件相关的结构体:/* File event structure */typedef struct aeFileEvent {    intmask; /* one of AE_(READABLE|WRITABLE) */   aeFileProc *rfileProc;  /* 指向有读事件调用的处理函数 */   aeFileProc *wfileProc;  /* 指向有写事件调用的处理函数 */    void*clientData;       /* 指向私有数据, 当做可读/可写函数的入参 */} aeFileEvent; /* A fired event */typedef struct aeFiredEvent {    int fd;    intmask;} aeFiredEvent;
  在aeEventLoop初始化的时候会为aeFileEvent数组(events)分配空间, 数组的大小由参数setsize指定,表明了当前Redis最大打开的套接字的大小, 套接字与aeFileEvent一一对应, 也就是说我们可以通过套接字数值作为索引到events数组中找到他对应的aeFileEvent对象。
  当我们在aeEventLoop中注册一个文件事件时, 首先我们判断传入的套接字对events数组是否有越界行为, 若没有越界行为, 我们便可以获取与当前套接字对应的aeFileEvent对象, 然后调用aeApiAddEvent将当前的文件描述符以及监听的事件注册到底层IO多路复用机制(epoll, select, evport, kqueue其中之一)中, 另外我们还需要指定当可读/可写事件发生时需要调用的函数,另外当前文件事件的一些私有数据被存放在clientData指向的对象当中。
  int aeCreateFileEvent(aeEventLoop *eventLoop,int fd, int mask,       aeFileProc *proc, void *clientData){    if(fd >= eventLoop->setsize) {       errno = ERANGE;       return AE_ERR;    }   aeFileEvent *fe = &eventLoop->events[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;    /*当前fd如果大于了目前的maxfd, 则需要进行更新 */    if(fd > eventLoop->maxfd)       eventLoop->maxfd = fd;    returnAE_OK;}
  三、时间事件
  Redis内部的时间事件实际可以分为两类, 一类是定时事件, 也就是需要在未来某一个时间点触发的事件(只触发一次), 另外一类是周期性事件,和前面的定时事件只触发一次不同, 周期性事件是每隔一段时间又会重新触发一次。
  Redis使用了timeProc指向函数的返回值来判断当前属于哪类事件, 若函数返回AE_NOMORE(也就是-1),说明当前事件无需再次触发(将id置删除标记AE_DELETED_EVENT_ID), 若函数返回一个大于等于0的值n, 说明再等待n秒, 该事件需要再重新被触发(根据返回值更新when_sec和when_ms),在博客开头提到的serverCron时间事件实际上就是一个周期性事件, 函数末尾会返回1000/server.hz, server.hz默认被设置为10, 也就是说serverCron平均每间隔100ms会被调用一次。/* Time event structure */typedef struct aeTimeEvent {    longlong id;             /* time eventidentifier, 定时事件的ID */    longwhen_sec;            /* seconds, 定时事件下一次触发的时间(秒) */    longwhen_ms;             /* milliseconds, 定时事件下一次触发的时间(毫秒) */   aeTimeProc *timeProc;     /* 指向定时事件发生时的调用函数 */   aeEventFinalizerProc *finalizerProc; /* 指向当当前定时事件被删除时的清理函数 */    void*clientData;         /* 指向私有数据, 当做定时事件函数的入参 */    structaeTimeEvent *next; /* 指向下一个定时事件的指针 */} aeTimeEvent;
  Redis调用aeCreateTimeEvent来创建一个时间任务, 实现非常简单, 传参我们关注一下milliseconds和proc即可, 前者指定了时间事件距离当前的触发时间, 后者指定了时间事件触发时应调用的函数, 内部通过aeAddMillisecondsToNow将当前定时任务触发的时间戳计算出来赋值给when_sec和when_ms, 然后再将timeProc指向时间事件到达时应该调用的函数。
  在完成了aeTimeEvent结构体内部变量赋值之后, 最后将其添加到aeEventLoop内部的存储定时间事件的环形链表的头部中(这里需要注意的是, 由于我们总是将新的时间事件加入环形链表的头部, 所以时间事件触发的时间先后并不是在环形链表中有序的, 我们需要将环形链表遍历完毕才能保证当前已经到达的时间事件都已经被处理完毕, 不过由于在开头提到过, 目前Redis只存在serverCron一个时间事件, 所以我们无需担心遍历环形链表影响服务性能), 此时一个时间事件就算创建完成了。static void aeAddMillisecondsToNow(long longmilliseconds, long *sec, long *ms) {    longcur_sec, cur_ms, when_sec, when_ms;     aeGetTime(&cur_sec,&cur_ms);   when_sec = cur_sec + milliseconds/1000;   when_ms = cur_ms + milliseconds%1000;    if(when_ms >= 1000) {       when_sec ++;       when_ms -= 1000;    }    *sec= when_sec;    *ms= when_ms;} long long aeCreateTimeEvent(aeEventLoop *eventLoop,long long milliseconds,       aeTimeProc *proc, void *clientData,       aeEventFinalizerProc *finalizerProc){    longlong id = eventLoop->timeEventNextId++;   aeTimeEvent *te;    te = zmalloc(sizeof(*te));    if(te == NULL) return AE_ERR;   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;    returnid;}
  Redis通过aeDeleteTimeEvent函数来删除一个时间任务, 传参只有一个待删除时间事件的id, 我们发现这里的删除实际上是一种惰性删除, 将aeTimeEvent中的id标记为AE_DELETED_EVENT_ID, 而不是直接将aeTimeEvent对象从链表中删除并且释放, 个人认为这么实现的原因更多是为了安全考虑以及代码的简洁性, 考虑在一个时间事件中本来想删除另外一个时间事件, 但是由于id填错, 误删成自己了, 此时如果释放自身aeTimeEvent对象, 这是十分危险的。int aeDeleteTimeEvent(aeEventLoop *eventLoop,long long id){   aeTimeEvent *te = eventLoop->timeEventHead;    while(te){       if (te->id == id) {           te->id = AE_DELETED_EVENT_ID;           return AE_OK;       }       te = te->next;    }    returnAE_ERR; /* NO event with the specified ID found */}
  四、事件的调度与执行
  Redis是单线程的, 内部是一直处于aeMain中的while循环中, 而循环内部不断调用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函数内部做的事情也非常简单, 下面进行了梳理:
  1. 首先调用aeSearchNearestTimer获取到达时间距离当前最近的时间事件;
  2. 计算上一步获取到的时间事件还有多久才可以触发, 并且将结果记录到一个struct timeval*指针指向的结构体中(若在步骤一中没有获取到时间事件对象, 那么指针为NULL);
  3. 阻塞并等待文件事件的产生, 最大的阻塞时间由步骤二决定(步骤二指针为NULL的场景表示当前没有时间事件, 我们可以永远阻塞, 直到有文件事件到达);
  4. 如果在最大阻塞时间内获取到了文件事件, 则根据文件事件的类型调用对应的读事件处理函数或者写事件处理函数;
  5. 遍历时间事件链表, 在这个过程中可能会遇到id为AE_DELETED_EVENT_ID的代表已经做了删除标记的时间事件, 需要将该时间事件从链表中移除, 并且进行释放, 如遇到已经达到的时间事件, 则调用其绑定的处理函数, 并且根据返回值来判断该事件时间是否需要在给定的时间内再重新触发。
  五、问题
  Q1: 时间事件触发的时间一定精准么?
  A1: 时间事件的触发并不能在指定的时间精准触发, 一般都要比指定的时间稍晚一点, 此外在Redis单线程模型下, 时间事件都是串行执行的, 中间如果某个时间事件处理时间长, 更加影响了后面时间事件执行时间的精准性. 而且时间事件链表是无序的, 所以在极端场景下, 存在优先级低的时间事件比优先级高的事件先触发的可能性, 不过好在目前Redis内部只有一个时间事件, 所以影响不会太大.
  Q2: aeEventLoop在创建之初就指定了可监听文件描述符的数量, 之后又通过config set maxclients命令动态调整客户端最大连接数是怎么实现的?
  A2: 通过翻看源码了解到, aeEventLoop提供了aeResizeSetSize函数, 用户重新分配events和fired数组的大小, 使aeEventLoop可监听的套接字数量得以调整, 当新的maxclients比原先要大的时候, 会调用该函数, 扩大aeEventLoop可监听文件描述符的数量, 以支持更多的客户端连接.int aeResizeSetSize(aeEventLoop *eventLoop,int setsize) {    inti;     if(setsize == eventLoop->setsize) return AE_OK;    if(eventLoop->maxfd >= setsize) return AE_ERR;    if(aeApiResize(eventLoop,setsize) == -1) return AE_ERR;    eventLoop->events = zrealloc(eventLoop->events,sizeof(aeFileEvent)*setsize);   eventLoop->fired = zrealloc(eventLoop->fired,sizeof(aeFiredEvent)*setsize);   eventLoop->setsize = setsize;     /*Make sure that if we created new slots, they are initialized with    * an AE_NONE mask. */    for(i = eventLoop->maxfd+1; i < setsize; i++)       eventLoop->events[i].mask = AE_NONE;    returnAE_OK;}
  六、总结
  Redis对事件的处理方式十分巧妙, 文件事件和时间事件之间相互配合, 充分的利用时间事件达到之前的这段时间等待和处理文件事件, 这样既避免了CPU的空转检查, 也能及时的处理文件事件. 此外通过时间事件中timeProc函数的返回值, 将时间事件的移除和再次触发权完全交给了用户, 使用起来更加灵活.
  微信公众号:杨建荣的学习笔记
  Github:@jeanron100
  CSDN:@jeanron100
  知乎:@jeanron100
  头条号:@杨建荣的学习笔记
  网易号:@杨建荣的数据库笔记
  大鱼号:@杨建荣的数据库笔记
  百家号:@杨建荣的数据库笔记
  腾讯云+社区:@杨建荣的学习笔记

宝宝什么时候添加辅食?绝大多数的宝宝在46个月左右即可以开始添加辅食,辅食的种类比较多,家长可以到正规超市或者母婴用品商店购买,也可以自行在家制作。购买的辅食包括米粉或者一些泥糊状的食物,通常营养都是比女孩子最受欢迎身高不是165cm?理想身高遭嫌弃,确实矮了点文章纯属原创,版权归本作者所有,欢迎个人转发分享一个人的身高影响着他整体的形象,所以现在父母对于孩子的身高也十分的重视。每个父母都希望孩子能够拥有一个理想的身高,这样孩子在成长过程胎儿这样的表现,可不是淘气鬼,而是聪明的表现自从感觉到胎儿后,小路开始有点焦虑,她感觉自己的一定生了一个淘气的宝宝,简直太欢快了,自己的肚皮上时常被宝宝顶起一个包或者踹出一个小脚丫。起先她甚至怀疑过,宝宝运动如此激烈是不是遇自己吐到虚脱,朋友却从不孕吐,听完医生解释,宝妈表示不羡慕怀孕对于有的宝妈来说是比较轻松比较幸福的,因为整个怀孕期间自己没有太大的反应,也不影响自己的日常生活。而对于有的宝妈来说就没有那么幸福,宝妈整天除了提心吊胆就是身体和心灵上的折磨,为什么有的家庭一直生不出儿子?可能与男方有关,来了解一下生儿子传宗接代的想法一直延续了数千年,虽说现在已经追求人人平等,但在部分地区还是有较为强烈的生儿子的思想。有些人没有重男轻女的思想,但仍然想生儿子,为的就是儿女双全,凑成一个好字。儿童节临近网红玩具要小心来源科技日报儿童节快到了这些网红玩具千万别送孩子六一国际儿童节将至,宠娃大军们又准备为孩子精心挑选礼物,线下实体商场线上购物平台也为儿童节促销开始预热。儿童玩具不断推陈出新,其艳丽妈妈,求求你别吼我了!这些极端情绪,正在戳痛你的孩子作者主创团哎呀妈有位妈妈,曾主动向超级育儿师节目组求助。她的诉求很简单,就是希望育儿专家兰海能帮她改变儿子爱哭闹赖床的坏习惯。可兰海接触这个家庭后,却发现妈妈的问题。当儿子走在路上做剖腹产打麻醉,以后会对腰有影响吗?有人说,女人生一次孩子,就等同于在鬼门关上走一遭,有过生育的女性,对此是有切身体会的。在剖腹产出现以前,有很多的女性死于难产,剖腹产的产生,挽救了太多女性和孩子的生命,让一个个家庭婴儿每次拉粑粑后应该水冲洗还是直接用棉柔巾擦?婴儿每次拉粑粑后应该用水冲洗还是直接用绵柔巾擦?这两个方法都不够科学,直接用水冲洗一个人操作起来不方便,直接用棉柔巾擦容易擦不干净,导致宝宝屁屁上有残留物,容易长红屁屁。很多妈妈会你在医院经历过哪些奇葩的人事?我继母住院期间,来了一位患者。来时是用那种带轱辘的床推来的,四个大男人按着,进来后几个人合力抬到病床上,在病床上就翻滚着,头使劲的抬起,眼睛骨碌碌的瞪着,那几个人就按着她不让她乱动孕妇喝什么汤比较下奶?刚出月子的美食博主来答什么汤比较下奶这个问题。事实上,下奶的汤针对每个人都是有所不同的,有人喝猪蹄汤灵,有人喝骨汤灵,有人喝鲫鱼汤灵,有的人喝什么汤都不管用,还有人什么都不喝奶都多
每次觉得接近大团圆时,都会有新的波澜当初,我看到车站母子相拥的时刻,以为这就是大团圆的结局。中间,也看到父母舅舅与儿子儿媳团聚吃饭的镜头,觉得格外温馨,想来这可能是片尾花絮,给所有关注这个新闻的人一个交代。就像所有电明确了7月1日起,公职人员兼职取薪篡改伪造档案都会被开除来源人事一号文前言常听人说,机关事业单位是铁饭碗,社会地位高福利好工作稳定。但进入机关事业单位真的就可以高枕无忧了吗?实则不然。刚刚(6月20日),全国人大通过了一部重磅法律中华人干货分享与eye有关的实用口语,你知道几个?眼睛是心灵之窗。在生活中,总是不乏和眼有关的词汇和句子。比如表达情感的望穿秋水含情脉脉,表达情绪的翻白眼眉开眼笑表达主观意识的睁一只眼闭一只眼等。今天我们就来学学和眼有关的英语表达比Iamsorry更好用的道歉方式,让你的口语听起来礼貌又地道在生活中,我们难免会有一些做错事情,或者不小心办错事情的时候,那么这个时候我们往往会说一句Imsorry,可你知道在不同的情况下道歉也应该用不同的方式来表达吗?下面就跟小E来学习一亲子共读3年!后悔没早点看到这个阅读锦囊你或许拥有无限的财富,一箱箱珠宝与一柜柜的黄金。但你永远不会比我富有,我有一位读书给我听的妈妈史斯克兰吉利兰世界上有这样子的一种亲密,叫做和孩子一起阅读。想象一下,小小软软的身体,取钱怎么说?takemoney?那你就真的错了你真的知道取钱的英文怎么说吗?可能你说错了很多年哦!01hr取钱不能说takemoney取钱的正确说法是drawmoneyMayIdrawmoneyagainsttheletter原始人的思维方式人类智慧的三个级别和表现形式本文原创陈旭网名屠龙有术01之前啊,很多年以前,陈旭讲过一个公式金钱权力智慧我们日常生活中的一切,生老病死,荣辱富贵,就是这个公式决定的。我们所说的成功,通常是指获得金钱和权力。而比Iamsorry更好用的道歉方式,让你的口语听起来礼貌又地道在生活中,我们难免会有一些做错事情,或者不小心办错事情的时候,那么这个时候我们往往会说一句Imsorry,可你知道在不同的情况下道歉也应该用不同的方式来表达吗?下面就跟小E来学习一别看到beef就以为是牛肉,这样翻译会很傻当老外跟你说Whatsyourbeef?你会不会以为他是问你你的牛肉是啥?那你就大错特错了因为这句话和牛肉没有任何关系哦今天,小E带大家一起来看看这些和吃相关的短语该怎么翻译!Wh出国住宿怎么选?HomestayorCouchsurfing?出国留学很多同学最先选择Homestay!中文称家庭寄宿。这种方式既可以更好的适应当地的生活,提高自己的语言能力,相对于其他的住宿方式又比较安全。所以很受留学生欢迎!HomestaOrange竟然不是橘子?橘子的英文到底怎么说?橘子用英文如何翻译?你脑海里浮现的词一定是orange毕竟从小学开始我们学到的就是orange表示橘子或橙子教科书上也常常把橘子和橙子划等号但实际上,orange只能指橙子oran