问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

面试官:Redis单线程已经很快,为何6.0要引入多线程?有啥优势?

发布网友 发布时间:2024-09-26 10:30

我来回答

1个回答

热心网友 时间:2024-11-06 13:32

Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis带来一些问题:

只能使用CPU一个核;

如果删除的键过大(比如Set类型中有上百万个对象),会导致服务端阻塞好几秒;

QPS难再提高。

针对上面问题,Redis在4.0版本以及6.0版本分别引入了LazyFree以及多线程IO,逐步向多线程过渡,下面将会做详细介绍。

单线程原理

都说Redis是单线程的,那么单线程是如何体现的?如何支持客户端并发请求的?为了搞清这些问题,首先来了解下Redis是如何工作的。

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

文件事件:

Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作,比如连接accept,read,write,close等;

时间事件:

Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象,比如过期键清理,服务状态统计等。

如上图,Redis将文件事件和时间事件进行抽象,时间轮训器会监听I/O事件表,一旦有文件事件就绪,Redis就会优先处理文件事件,接着处理时间事件。在上述所有事件处理上,Redis都是以单线程形式处理,所以说Redis是单线程的。

此外,如下图,Redis基于Reactor模式开发了自己的I/O事件处理器,也就是文件事件处理器,Redis在I/O事件处理上,采用了I/O多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数,通过一个线程实现了多客户端并发处理。

正因为这样的设计,在数据处理上避免了加锁操作,既使得实现上足够简洁,也保证了其高性能。当然,Redis单线程只是指其在事件处理上,实际上,Redis也并不是单线程的,比如生成RDB文件,就会fork一个子进程来实现,当然,这不是本文要讨论的内容。

LazyFree机制

如上所知,Redis在处理客户端命令时是以单线程形式运行,而且处理速度很快,期间不会响应其他客户端请求,但若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,导致服务器卡住好几秒,对负载较高的缓存系统而言将会是个灾难。为了解决这个问题,在Redis4.0版本引入了LazyFree,将慢操作异步化,这也是在事件处理上向多线程迈进了一步。

如作者在其博客中所述,要解决慢操作,可以采用渐进式处理,即增加一个时间事件,比如在删除一个具有上百万个对象的Set键时,每次只删除大键中的一部分数据,最终实现大键的删除。但是,该方案可能会导致回收速度赶不上创建速度,最终导致内存耗尽。

因此,Redis最终实现上是将大键的删除操作异步化,采用非阻塞删除(对应命令UNLINK),大键的空间回收交由单独线程实现,主线程只做关系解除,可以快速返回,继续处理其他事件,避免服务器长时间阻塞。

以删除(DEL命令)为例,看看Redis是如何实现的,下面就是删除函数的入口,其中,lazyfree_lazy_user_del是是否修改DEL命令的默认行为,一旦开启,执行DEL时将会以UNLINK形式执行。

voiddelCommand(client*c){delGenericCommand(c,server.lazyfree_lazy_user_del);}/*ThiscommandimplementsDELandLAZYDEL.*/voiddelGenericCommand(client*c,intlazy){intnumdel=0,j;for(j=1;j<c->argc;j++){expireIfNeeded(c->db,c->argv[j]);//根据配置确定DEL在执行时是否以lazy形式执行intdeleted=lazy?dbAsyncDelete(c->db,c->argv[j]):dbSyncDelete(c->db,c->argv[j]);if(deleted){signalModifiedKey(c,c->db,c->argv[j]);notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[j],c->db->id);server.dirty++;numdel++;}}addReplyLongLong(c,numdel);}

同步删除很简单,只要把key和value删除,如果有内层引用,则进行递归删除,这里不做介绍。下面看下异步删除,Redis在回收对象时,会先计算回收收益,只有回收收益在超过一定值时,采用封装成Job加入到异步处理队列中,否则直接同步回收,这样效率更高。回收收益计算也很简单,比如String类型,回收收益值就是1,而Set类型,回收收益就是集合中元素个数。

/*Deleteakey,value,andassociatedexpirationentryifany,fromtheDB.*Ifthereareenoughallocationstofreethevalueobjectmaybeputinto*alazyfreelistinsteadofbeingfreedsynchronously.Thelazyfreelist*willbereclaimedinadifferentbio.cthread.*/#defineLAZYFREE_THRESHOLD64intdbAsyncDelete(redisDb*db,robj*key){/*Deletinganentryfromtheexpiresdictwillnotfreethesdsof*thekey,becauseitissharedwiththemaindictionary.*/if(dictSize(db->expires)>0)dictDelete(db->expires,key->ptr);/*Ifthevalueiscomposedofafewallocations,tofreeinalazyway*isactuallyjustslower...Sounderacertainlimitwejustfree*theobjectsynchronously.*/dictEntry*de=dictUnlink(db->dict,key->ptr);if(de){robj*val=dictGetVal(de);//计算value的回收收益size_tfree_effort=lazyfreeGetFreeEffort(val);/*Ifreleasingtheobjectistoomuchwork,doitinthebackground*byaddingtheobjecttothelazyfreelist.*Notethatiftheobjectisshared,toreclaimitnowitisnot*possible.Thisrarelyhappens,howeversometimestheimplementation*ofpartsoftheRediscoremaycallincrRefCount()toprotect*objects,andthencalldbDelete().Inthiscasewe'llfall*throughandreachthedictFreeUnlinkedEntry()call,thatwillbe*equivalenttojustcallingdecrRefCount().*///只有回收收益超过一定值,才会执行异步删除,否则还是会退化到同步删除if(free_effort>LAZYFREE_THRESHOLD&&val->refcount==1){atomicIncr(lazyfree_objects,1);bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);dictSetVal(db->dict,de,NULL);}}/*Releasethekey-valpair,orjustthekeyifwesettheval*fieldtoNULLinordertolazyfreeitlater.*/if(de){dictFreeUnlinkedEntry(db->dict,de);if(server.cluster_enabled)slotToKeyDel(key->ptr);return1;}else{return0;}}

通过引入athreadedlazyfree,Redis实现了对于SlowOperation的Lazy操作,避免了在大键删除,FLUSHALL,FLUSHDB时导致服务器阻塞。当然,在实现该功能时,不仅引入了lazyfree线程,也对Redis聚合类型在存储结构上进行改进。

因为Redis内部使用了很多共享对象,比如客户端输出缓存。当然,Redis并未使用加锁来避免线程冲突,锁竞争会导致性能下降,而是去掉了共享对象,直接采用数据拷贝,如下,在3.x和6.x中ZSet节点value的不同实现。

//3.2.5版本ZSet节点实现,value定义robj*obj/*ZSETsuseaspecializedversionofSkiplists*/typedefstructzskiplistNode{robj*obj;doublescore;structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;unsignedintspan;}level[];}zskiplistNode;//6.0.10版本ZSet节点实现,value定义为sdsele/*ZSETsuseaspecializedversionofSkiplists*/typedefstructzskiplistNode{sdsele;doublescore;structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;unsignedlongspan;}level[];}zskiplistNode;

去掉共享对象,不但实现了lazyfree功能,也为Redis向多线程跨进带来了可能,正如作者所述:

Nowthatvaluesofaggregateddatatypesarefullyunshared,andclientoutputbuffersdon’tcontainsharedobjectsaswell,thereisalottoexploit.ForexampleitisfinallypossibletoimplementthreadedI/OinRedis,sothatdifferentclientsareservedbydifferentthreads.Thismeansthatwe’llhaveagloballockonlywhenaccessingthedatabase,buttheclientsread/writesyscallsandeventheparsingofthecommandtheclientissending,canhappenindifferentthreads.

多线程I/O及其局限性

Redis在4.0版本引入了LazyFree,自此Redis有了一个LazyFree线程专门用于大键的回收,同时,也去掉了聚合类型的共享对象,这为多线程带来可能,Redis也不负众望,在6.0版本实现了多线程I/O。

实现原理

正如官方以前的回复,Redis的性能瓶颈并不在CPU上,而是在内存和网络上。因此6.0发布的多线程并未将事件处理改成多线程,而是在I/O上,此外,如果把事件处理改成多线程,不但会导致锁竞争,而且会有频繁的上下文切换,即使用分段锁来减少竞争,对Redis内核也会有较大改动,性能也不一定有明显提升。

如上图红色部分,就是Redis实现的多线程部分,利用多核来分担I/O读写负荷。在事件处理线程每次获取到可读事件时,会将所有就绪的读事件分配给I/O线程,并进行等待,在所有I/O线程完成读操作后,事件处理线程开始执行任务处理,在处理结束后,同样将写事件分配给I/O线程,等待所有I/O线程完成写操作。

以读事件处理为例,看下事件处理线程任务分配流程:

inthandleClientsWithPendingReadsUsingThreads(void){.../*DistributetheclientsacrossNdifferentlists.*/listIterli;listNode*ln;listRewind(server.clients_pending_read,&li);intitem_id=0;//将等待处理的客户端分配给I/O线程while((ln=listNext(&li))){client*c=listNodeValue(ln);inttarget_id=item_id%server.io_threads_num;listAddNodeTail(io_threads_list[target_id],c);item_id++;}.../*Waitforalltheotherthreadstoendtheirwork.*///轮训等待所有I/O线程处理完while(1){unsignedlongpending=0;for(intj=1;j<server.io_threads_num;j++)pending+=io_threads_pending[j];if(pending==0)break;}...returnprocessed;}

I/O线程处理流程:

void*IOThreadMain(void*myid){...while(1){...//I/O线程执行读写操作while((ln=listNext(&li))){client*c=listNodeValue(ln);//io_threads_op判断是读还是写事件if(io_threads_op==IO_THREADS_OP_WRITE){writeToClient(c,0);}elseif(io_threads_op==IO_THREADS_OP_READ){readQueryFromClient(c->conn);}else{serverPanic("io_threads_opvalueisunknown");}}listEmpty(io_threads_list[id]);io_threads_pending[id]=0;if(tio_debug)printf("[%ld]Done\n",id);}}局限性

从上面实现上看,6.0版本的多线程并非彻底的多线程,I/O线程只能同时执行读或者同时执行写操作,期间事件处理线程一直处于等待状态,并非流水线模型,有很多轮训等待开销。

Tair多线程实现原理

相较于6.0版本的多线程,Tair的多线程实现更加优雅。如下图,Tair的MainThread负责客户端连接建立等,IOThread负责请求读取、响应发送、命令解析等,WorkerThread线程专门用于事件处理。IOThread读取用户的请求并进行解析,之后将解析结果以命令的形式放在队列中发送给WorkerThread处理。WorkerThread将命令处理完成后生成响应,通过另一条队列发送给IOThread。为了提高线程的并行度,IOThread和WorkerThread之间采用无锁队列和管道进行数据交换,整体性能会更好。

小结

Redis4.0引入LazyFree线程,解决了诸如大键删除导致服务器阻塞问题,在6.0版本引入了I/OThread线程,正式实现了多线程,但相较于Tair,并不太优雅,而且性能提升上并不多,压测看,多线程版本性能是单线程版本的2倍,Tair多线程版本则是单线程版本的3倍。在作者看来,Redis多线程无非两种思路,I/Othreading和Slowcommandsthreading,正如作者在其博客中所说:

I/OthreadingisnotgoingtohappeninRedisAFAIK,becauseaftermuchconsiderationIthinkit’salotofcomplexitywithoutagoodreason.ManyRedissetupsarenetworkormemoryboundactually.AdditionallyIreallybelieveinashare-nothingsetup,sothewayIwanttoscaleRedisisbyimprovingthesupportformultipleRedisinstancestobeexecutedinthesamehost,especiallyviaRedisCluster.

WhatinsteadIreallywantalotisslowoperationsthreading,andwiththeRedismolessystemwealreadyareintherightdirection.Howeverinthefuture(notsureifinRedis6or7)we’llgetkey-levellockinginthemolesystemsothatthreadscancompletelyacquirecontrolofakeytoprocessslowoperations.Nowmolescanimplementcommandsandcancreateareplyfortheclientinacompletelyseparatedway,butstilltoaccesstheshareddatasetagloballockisneeded:thiswillgoaway.

Redis作者更倾向于采用集群方式来解决I/Othreading,尤其是在6.0版本发布的原生RedisClusterProxy背景下,使得集群更加易用。

此外,作者更倾向于slowoperationsthreading(比如4.0版本发布的LazyFree)来解决多线程问题。后续版本,是否会将IOThread实现的更加完善,采用Mole实现对慢操作的优化,着实值得期待。

作者:Java斗帝之路
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
武汉地铁能把滑板带上去么 铜钱草怎么种植方法 回收冬虫夏草一斤多少钱 ...在到臀部,和腿上都有了。时好时坏,谁也睡不好,求好好心人帮助... 我的身体有点很痒总是很想去挠后背也很疼全身都是很痒... 得了一种皮肤病 先疼后痒 试过很多药 没有 用 乞求社会帮帮我 人身体为什么会痒 我全身很痒到底是什么病求大神帮助 ...是奇痒难忍不红不肿,去医院做抽血查过敏源也没问题,谁能帮帮我... ...如下.(单位:千瓦时)6月底7月底8月底9月底10月底11月底12月底4305456407... "scratching"是什么意思 手机号码信息泄露怎么处理教程图片 牛牛坝水电站机电设备 水轮发电机型号有哪些 高级汽车修理工在部队有用吗 当兵要是去汽车连了!会一直在部队当修理工吗 怎么在部队里当一名飞机修理工 高级汽修修理工在部队服役待遇如何 部队要不要修理工 部队要不要修理工!我是一名修理厂想在部队发展但不... 中国移动的彩铃设置方法是什么? 中国移动的手机彩铃怎么设置? 合景泰富地产排名 黑办发 1983 76号 我忘记有一部日本动漫科幻电影的名字了,描述在问题补充里面。_百度知 ... 破刃之剑剧场版动画制作 全球十大车企市值排名榜(比亚迪市值全球车企排名第几) 35w疝气灯用什么样的电瓶好时间长 两个12V55w氙气灯用多大的电瓶不亏电 35w4300k的疝气灯。我用15AH的电瓶能用多久。 诺基亚N8内存和处理器问题 ...要求:1.至少有两处细节描写 2.首尾呼应 3.400字 速求 急!!!_百... 1969年阴历冬月二十四生日属于什么星座 一九六九年阴历二月二十四出生的是什么星座 微信账号可以单卖吗 广东考清华北大多少人? 德国留学第一年费用 微信账号能卖出去吗? 一个手机号注册了两个微信号可以取消吗? python输出公式结果为百分数是多少 Python学习:输入与输出教程 python,%1.1f%%',这个格式是什么意思,我看懂的是字段宽1,精度1,后面的... python小数怎么化成分数是多少 网课期间孩子沉迷游戏怎么办 青少年沉迷网络游戏怎么办 未成年沉迷网络的危害 如何让孩子远离网游的危害呢? 想知道:江西省 从江西省九江市庐山区庐山大道到江西省九江市德安县怎么... 从庐山区海会镇到九江市新华书店怎么走? 身份证号码怎样才能正常显示出来 ...丙子年戊戌月戊子日甲寅时生 求大神解答 看看到底怎么样?好不好呀... 丙子年戊戌月庚辰日寅时生的男子,怎么样?