生活工程体验信仰哲学精神
投稿投诉
精神世界
探索历史
哲学文学
艺术价值
信仰创造
境界审美
体验技术
技能工具
工程信息
医学生产
生活运用
操作能力

Redis为什么这么快?

10月22日 天浪楼投稿
  文章转载自腾讯云数据库
  作者吴显坚,腾讯云数据库高级工程师,参与过360开源项目Pika的研发工作,现从事redis数据库研发工作。
  Redis服务器是一个事件驱动程序,事件是Redis服务器的核心,它处理两项重要的任务,一个是IO事件(文件事件),另外一个是时间事件。Redis服务器通过套接字与客户端进行连接,而文件事件可以理解为服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。另外Redis内部有一些操作(从Redis4。0的代码分析目前时间事件只有serverCron)需要在给定的时间点执行,而时间事件就是Redis服务器对这类定时操作的抽象。
  一、aeEventLoop
  在分析具体代码之前,我们先了解一下在事件处理中处于核心部分的aeEventLoop到底是什么:StateofaneventbasedprogramtypedefstructaeEventLoop{highestfiledescriptorcurrentlyregistered,当前注册的最大fd数maxnumberoffiledescriptorstracked,允许关注fd数量的上限(就是events和fired数组的大小)longlongtimeEventNextId下一个定时事件的ID;timetlastTUsedtodetectsystemclockskewaeFileERegisteredevents,指向aeFileEvent数组,每个fd都有对应的aeFileEvent对象(fd就是当做下标用于索引)aeFiredEFiredevents,指向aeFiredEvent数组,当检测到有文件事件时,会将对应的fd和事件存放在数组里面aeTimeEventtimeEventHead指向首个时间事件结构体,而时间事件结构体里有next指针,指向下一个结构体,实际上整体看上去是一个环形链表(最后一个时间事件结构体里的next指针会指向timeEventHintstop是否停止eventloop循环;ThisisusedforpollingAPIspecificdata,指向底层不同多路复用实现的数据结构,可以是epoll,select,evport或者是kqueueaeBeforeSleepPaeBeforeSleepP}aeEventL
  创建aeEventLoop只需要一个setsize参数,它标识了当前aeEventLoop最大可以监听的文件描述符数(通常redis传入server。maxclientsCONFIGFDSETINCR,也就是在用户指定的最大客户端连接数的基础上再额外增加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数组的前三个元素会被赋值{fd6,maskAEREADABLE},{fd8,maskAEREADABLE},{fd10,maskAEWRITABLE},紧接着我们以6为索引,找到文件事件数据结构events〔6〕,然后发现触发的是可读事件,我们再调用events〔6〕中rfileProc来处理可读事件。
  aeEventLoopaeCreateEventLoop(intsetsize){aeEventLoopeventLif((eventLoopzmalloc(sizeof(eventLoop)))NULL)eventLoopeventszmalloc(sizeof(aeFileEvent)setsize);eventLoopfiredzmalloc(sizeof(aeFiredEvent)setsize);if(eventLoopeventsNULLeventLoopfiredNULL)eventLeventLooplastTimetime(NULL);eventLooptimeEventHeadNULL;eventLooptimeEventNextId0;eventLoopstop0;eventLoopmaxfd1;eventLoopbeforesleepNULL;eventLoopaftersleepNULL;if(aeApiCreate(eventLoop)1)EventswithmaskAENONEarenotset。Soletsinitializethevectorwithit。for(i0;i)eventLoopevents〔i〕。maskAENONE;returneventL}
  对于时间事件,aeEventLoop中有一个timeEventHead指针指向第一个时间事件,由于aeEventLoop创建之初,内部没有任何时间事件,所以初始化时timeEventHead指向NULL,每当有新的时间事件时,总会被添加到timeEventHead头部,由于aeTimeEvent结构体中有next指针可以指向下一个aeTimeEvent结构体,所以只要我们获取timeEventHead就能遍历当前所有的时间事件了,另外有一个细节需要注意,最后一个aeTimeEvent结构体中的next指针指向的是timeEventHead,所以所有时间事件实际上就是由一个环形链表串连起来的。
  二、文件事件
  在介绍中有提到过文件事件实际上就是服务器对套接字操作的抽象,当套接字有可读写事件触发的时候,我们需要调用相应的处理函数,下面先看一下跟文件事件相关的结构体:FileeventstructuretypedefstructaeFileEvent{oneofAE(READABLEWRITABLE)aeFileProcrfileP指向有读事件调用的处理函数aeFileProcwfileP指向有写事件调用的处理函数voidclientD指向私有数据,当做可读可写函数的入参}aeFileEAfiredeventtypedefstructaeFiredEvent{}aeFiredE
  在aeEventLoop初始化的时候会为aeFileEvent数组(events)分配空间,数组的大小由参数setsize指定,表明了当前Redis最大打开的套接字的大小,套接字与aeFileEvent一一对应,也就是说我们可以通过套接字数值作为索引到events数组中找到他对应的aeFileEvent对象。
  当我们在aeEventLoop中注册一个文件事件时,首先我们判断传入的套接字对events数组是否有越界行为,若没有越界行为,我们便可以获取与当前套接字对应的aeFileEvent对象,然后调用aeApiAddEvent将当前的文件描述符以及监听的事件注册到底层IO多路复用机制(epoll,select,evport,kqueue其中之一)中,另外我们还需要指定当可读可写事件发生时需要调用的函数,另外当前文件事件的一些私有数据被存放在clientData指向的对象当中。
  intaeCreateFileEvent(aeEventLoopeventLoop,intfd,intmask,aeFileProcproc,voidclientData){if(fdeventLoopsetsize){errnoERANGE;returnAEERR;}aeFileEventfeeventLoopevents〔fd〕;if(aeApiAddEvent(eventLoop,fd,mask)1)returnAEERR;if(maskAEREADABLE)ferfilePif(maskAEWRITABLE)fewfilePfeclientDataclientD当前fd如果大于了目前的maxfd,则需要进行更新if(fdeventLoopmaxfd)eventLreturnAEOK;}
  三、时间事件
  Redis内部的时间事件实际可以分为两类,一类是定时事件,也就是需要在未来某一个时间点触发的事件(只触发一次),另外一类是周期性事件,和前面的定时事件只触发一次不同,周期性事件是每隔一段时间又会重新触发一次。
  Redis使用了timeProc指向函数的返回值来判断当前属于哪类事件,若函数返回AENOMORE(也就是1),说明当前事件无需再次触发(将id置删除标记AEDELETEDEVENTID),若函数返回一个大于等于0的值n,说明再等待n秒,该事件需要再重新被触发(根据返回值更新whensec和whenms),在博客开头提到的serverCron时间事件实际上就是一个周期性事件,函数末尾会返回1000server。hz,server。hz默认被设置为10,也就是说serverCron平均每间隔100ms会被调用一次。TimeeventstructuretypedefstructaeTimeEvent{timeeventidentifier,定时事件的IDseconds,定时事件下一次触发的时间(秒)milliseconds,定时事件下一次触发的时间(毫秒)aeTimeProctimeP指向定时事件发生时的调用函数aeEventFinalizerProcfinalizerP指向当当前定时事件被删除时的清理函数voidclientD指向私有数据,当做定时事件函数的入参structaeTimeE指向下一个定时事件的指针}aeTimeE
  Redis调用aeCreateTimeEvent来创建一个时间任务,实现非常简单,传参我们关注一下milliseconds和proc即可,前者指定了时间事件距离当前的触发时间,后者指定了时间事件触发时应调用的函数,内部通过aeAddMillisecondsToNow将当前定时任务触发的时间戳计算出来赋值给whensec和whenms,然后再将timeProc指向时间事件到达时应该调用的函数。
  在完成了aeTimeEvent结构体内部变量赋值之后,最后将其添加到aeEventLoop内部的存储定时间事件的环形链表的头部中(这里需要注意的是,由于我们总是将新的时间事件加入环形链表的头部,所以时间事件触发的时间先后并不是在环形链表中有序的,我们需要将环形链表遍历完毕才能保证当前已经到达的时间事件都已经被处理完毕,不过由于在开头提到过,目前Redis只存在serverCron一个时间事件,所以我们无需担心遍历环形链表影响服务性能),此时一个时间事件就算创建完成了。staticvoidaeAddMillisecondsToNow(longlongmilliseconds,longsec,longms){longcursec,curms,whensec,aeGetTime(cursec,curms);whenseccursecmilliseconds1000;whenmscurmsmilliseconds1000;if(whenms1000){whenms1000;}}longlongaeCreateTimeEvent(aeEventLoopeventLoop,longlongmilliseconds,aeTimeProcproc,voidclientData,aeEventFinalizerProcfinalizerProc){longlongideventLooptimeEventNextId;aeTimeEtezmalloc(sizeof(te));if(teNULL)returnAEERR;aeAddMillisecondsToNow(milliseconds,tewhensec,tewhenms);tetimePtefinalizerProcfinalizerPteclientDataclientDtenexteventLooptimeEventHeventLooptimeEventH}
  Redis通过aeDeleteTimeEvent函数来删除一个时间任务,传参只有一个待删除时间事件的id,我们发现这里的删除实际上是一种惰性删除,将aeTimeEvent中的id标记为AEDELETEDEVENTID,而不是直接将aeTimeEvent对象从链表中删除并且释放,个人认为这么实现的原因更多是为了安全考虑以及代码的简洁性,考虑在一个时间事件中本来想删除另外一个时间事件,但是由于id填错,误删成自己了,此时如果释放自身aeTimeEvent对象,这是十分危险的。intaeDeleteTimeEvent(aeEventLoopeventLoop,longlongid){aeTimeEventteeventLooptimeEventHwhile(te){if(teidid){teidAEDELETEDEVENTID;returnAEOK;}}returnAEERR;NOeventwiththespecifiedIDfound}
  四、事件的调度与执行
  Redis是单线程的,内部是一直处于aeMain中的while循环中,而循环内部不断调用aeProcessEvents函数,该函数会对上面提到的文件事件和时间事件进行调度,决定何时处理文件事件以及时间事件。voidaeMain(aeEventLoopeventLoop){eventLoopstop0;while(!eventLoopstop){if(eventLoopbeforesleep!NULL)eventLoopbeforesleep(eventLoop);aeProcessEvents(eventLoop,AEALLEVENTSAECALLAFTERSLEEP);}}
  实际上aeProcessEvents函数内部做的事情也非常简单,下面进行了梳理:
  1。首先调用aeSearchNearestTimer获取到达时间距离当前最近的时间事件;
  2。计算上一步获取到的时间事件还有多久才可以触发,并且将结果记录到一个structtimeval指针指向的结构体中(若在步骤一中没有获取到时间事件对象,那么指针为NULL);
  3。阻塞并等待文件事件的产生,最大的阻塞时间由步骤二决定(步骤二指针为NULL的场景表示当前没有时间事件,我们可以永远阻塞,直到有文件事件到达);
  4。如果在最大阻塞时间内获取到了文件事件,则根据文件事件的类型调用对应的读事件处理函数或者写事件处理函数;
  5。遍历时间事件链表,在这个过程中可能会遇到id为AEDELETEDEVENTID的代表已经做了删除标记的时间事件,需要将该时间事件从链表中移除,并且进行释放,如遇到已经达到的时间事件,则调用其绑定的处理函数,并且根据返回值来判断该事件时间是否需要在给定的时间内再重新触发。
  五、问题
  Q1:时间事件触发的时间一定精准么?
  A1:时间事件的触发并不能在指定的时间精准触发,一般都要比指定的时间稍晚一点,此外在Redis单线程模型下,时间事件都是串行执行的,中间如果某个时间事件处理时间长,更加影响了后面时间事件执行时间的精准性。而且时间事件链表是无序的,所以在极端场景下,存在优先级低的时间事件比优先级高的事件先触发的可能性,不过好在目前Redis内部只有一个时间事件,所以影响不会太大。
  Q2:aeEventLoop在创建之初就指定了可监听文件描述符的数量,之后又通过configsetmaxclients命令动态调整客户端最大连接数是怎么实现的?
  A2:通过翻看源码了解到,aeEventLoop提供了aeResizeSetSize函数,用户重新分配events和fired数组的大小,使aeEventLoop可监听的套接字数量得以调整,当新的maxclients比原先要大的时候,会调用该函数,扩大aeEventLoop可监听文件描述符的数量,以支持更多的客户端连接。intaeResizeSetSize(aeEventLoopeventLoop,intsetsize){if(setsizeeventLoopsetsize)returnAEOK;if(eventLoopmaxfdsetsize)returnAEERR;if(aeApiResize(eventLoop,setsize)1)returnAEERR;eventLoopeventszrealloc(eventLoopevents,sizeof(aeFileEvent)setsize);eventLoopfiredzrealloc(eventLoopfired,sizeof(aeFiredEvent)setsize);eventLMakesurethatifwecreatednewslots,theyareinitializedwithanAENONEmask。for(ieventLoopmaxfd1;i)eventLoopevents〔i〕。maskAENONE;returnAEOK;}
  六、总结
  Redis对事件的处理方式十分巧妙,文件事件和时间事件之间相互配合,充分的利用时间事件达到之前的这段时间等待和处理文件事件,这样既避免了CPU的空转检查,也能及时的处理文件事件。此外通过时间事件中timeProc函数的返回值,将时间事件的移除和再次触发权完全交给了用户,使用起来更加灵活。
  微信公众号:杨建荣的学习笔记
  Github:jeanron100
  CSDN:jeanron100
  知乎:jeanron100
  头条号:杨建荣的学习笔记
  网易号:杨建荣的数据库笔记
  大鱼号:杨建荣的数据库笔记
  百家号:杨建荣的数据库笔记
  腾讯云社区:杨建荣的学习笔记
投诉 评论 转载

1991年2月27日科威特恢复国家独立和主权在30年前的今天,1991年2月27日(农历1991年1月13日),科威特恢复国家独立和主权。1991年2月27日,科威特城这个近7个月来全球瞩目的城市,从早到晚到处响彻……4个不为人知的偏门,却能月入二三十万都说做生意要做高利润的暴利行业,像化妆品、保健品这些听起来利润很高的项目,比如一瓶精华成本40块钱能卖到400。但是这种产品和服务高度同质化的项目能不能赚钱,全靠营销手段,并且……如何正确诠释中国风格钢琴作品?抓住这3点就好文丨高立丽中国作曲家通过几代人的努力,以本民族的音乐语言作为创作的母语,进行了中国风格音乐创作的研究和实践。创作出了既具有中国民族特色,又具有鲜明时代风貌的钢琴作品。……70岁老太太的幸福医院进了个老太太,老年痴呆脑梗还是心梗我忘了。老伴名下有好几个上市公司,属于亿万富豪的档次吧。自然是最好的病房,最好的药物,最好的治疗,钱花的和流水一样。老太太时而清醒时……围绕中心意思写作文忙【忙】王良赫早晨,我睁开眼睛,就开始忙’起来,忙碌的一个小时,必须在最短时间内刷牙,洗脸、吃饭。时间充足的话,下楼练习踢毽子,回来后手忙脚乱地穿上衣服,匆匆忙忙地赶……Redis为什么这么快?文章转载自腾讯云数据库作者吴显坚,腾讯云数据库高级工程师,参与过360开源项目Pika的研发工作,现从事redis数据库研发工作。Redis服务器是一个事件驱动程序……一文理清数据分析知识体系,我先肝为敬这几年,数据分析是很火啊,在这个数据驱动一切的时代,数据挖掘和数据分析就是这个时代的淘金,懂数据分析、拥有数据思维,往往成了大厂面试的加分项。比如通过数据分析,我们可以更……心底的感动现实生活中,自以为是一个理性、理智的人,但是,总是在别人的真情告白中潸然泪下,哪怕是在手机里的、电视中的。看不得真情流露,听不得别人的哭声,欣赏不得悲调的音乐,这些都会钩来我的……想让孩子长高有什么办法?有利于孩子长高的十种运动第一名:游泳第二名:篮球第三名:跳绳第四名:跑步第五名:蛙跳第六名:摸高第七名:羽毛球第八名:蹲马步……苏仙岭米饺与青蒿饺雾雨罩冬,润物无声。雾雨中的苏仙岭仿佛一幅水墨画。我们从郴州苏仙岭下来,在马路边遇到一食摊,蒸笼里热气腾腾,一股特殊辛香飘来,走近一看,我才发现是青蒿饺香。正吸引着一对外……伤感造句用伤感造句大全【伤感shnggn】:想到难过的事而悲伤:无限伤感伤感不已。(1)甚至伟人也有穷亲戚,这是一个令人伤感的事实。狄更斯(2)伤感一类的情绪,是对短暂的生命的浪费,实在……多脆皮孜然年糕怎么做脆皮孜然年糕教程介绍过年期间小吃少不了,这道脆皮孜然年糕就很适合春节期间在家做哦,人手一只炸年糕香味扑鼻,好吃的不得了。那么多脆皮孜然年糕怎么做?下面小编带来:脆皮孜然年糕教程介绍。脆皮孜然……
家里被蹭网光纤宽带还不够用教你遗愿那两个交换老婆的男人,十多年过去了,他们的现状怎样两极分化!农村老人和城里老人的不同,一个潇洒,一个心酸深重造句用深重造句大全蘑菇街旗下美丽金融还美丽吗科技赋能普惠金融金蛋理那本语文书浅析物理教学中探究式教学法的运用藏缘命如果打分的话给你的婚姻打几分?满分10分一堆你没有见过的黑科技工具鱼缸有过滤器还需要氧气泵吗有过滤泵还要氧气泵吗

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找新乡渭南铜川松江山南雅安松原荃湾淮北昭通平凉鞍山赤峰苗栗保亭池州渝北株洲陇南濮阳三沙秀山密云鸡西