提到redis馬上在我們腦海中會(huì)浮現(xiàn)出這樣一些關(guān)鍵字:?jiǎn)尉€程、高性能煤篙、內(nèi)存數(shù)據(jù)庫斟览、kv存儲(chǔ)......這些關(guān)鍵字都從不同層面描述了redis的一些相關(guān)特性和技術(shù)實(shí)現(xiàn)。那么為什么redis具備這些特性以及是如何實(shí)現(xiàn)的辑奈,本文將進(jìn)行一一分析苛茂。
一、單線程
1.1 為什么是單線程
總結(jié)Redis的普通KV存儲(chǔ)瓶頸不在 CPU鸠窗,而往往可能受到內(nèi)存和網(wǎng)絡(luò)I/O 的制約妓羊。
Redis 中有多種類型的數(shù)據(jù)操作,甚至包括一些事務(wù)處理稍计,如果采用多線程躁绸,則會(huì)被多線程產(chǎn)生的切換問題而困擾,也可能因?yàn)榧渔i導(dǎo)致系統(tǒng)架構(gòu)變的異常復(fù)雜造成性能損耗臣嚣。
Redis作者咋說的:
總結(jié)來說就是對(duì)于redis來說單線程的設(shè)計(jì)能夠保證性能涨颜,多線程在設(shè)計(jì)和實(shí)現(xiàn)上會(huì)帶來更多的復(fù)雜度。但是使用單線程的方式確實(shí)無法很好發(fā)揮多核CPU 的性能茧球,可以通過在單機(jī)開多個(gè)Redis 實(shí)例來完善!
1.2 有多線程的考量嗎
Redis4.0版本對(duì)于一些大鍵值對(duì)的刪除操作星持,引入多線程來非阻塞地釋放內(nèi)存空間抢埋,能減少對(duì) Redis 主線程阻塞的時(shí)間,提高執(zhí)行的效率督暂。
Redis6.0 引入多線程來提高網(wǎng)絡(luò) IO 讀寫性能揪垄。
這里要注意的是Redis 的多線程只是在網(wǎng)絡(luò)數(shù)據(jù)的讀寫這類耗時(shí)操作上使用了, 執(zhí)行命令仍然是單線程順序執(zhí)行逻翁。Redis6中默認(rèn)是禁用多線程的饥努,可以通過修改redis的配置文件中io-threads-do-reads=true來開啟。除此之外還需要設(shè)置現(xiàn)場(chǎng)的數(shù)量才能正真開啟多線程八回,配置參數(shù)為io-threads 3表示開啟三個(gè)線程酷愧。
線程設(shè)置建議:關(guān)于線程數(shù)的設(shè)置驾诈,官方的建議是如果為 4 核的 CPU,建議線程數(shù)設(shè)置為 2 或 3溶浴,如果為 8 核 CPU 建議線程數(shù)設(shè)置為 6乍迄,線程數(shù)一定要小于機(jī)器核數(shù),線程數(shù)并不是越大越好士败。
二闯两、高性能
通常我們的理解是單線程性能沒有多線程好,那redis又是如何做到高性能的了谅将?
2.1 I/O多路復(fù)用
這是我們最多看到的一句解釋漾狼,redis使用了I/O多路復(fù)用的模式,所以性能高饥臂,那么到底什么是I/O多路復(fù)用模型逊躁,以及在redis中怎么實(shí)現(xiàn)的。我這里先打幾個(gè)比方來方便大家理解擅笔。
要過年了志衣,老王去火車站買票回家過年,春運(yùn)期間票不好買猛们,老王買了三天買到了一張退票念脯。這樣一個(gè)場(chǎng)景老王有三種方式來完成這次買票:
方式一:老王去到火車站售票大廳,在長(zhǎng)椅上躺了兩夜弯淘,終于在第三天等到了一張票绿店,興高采烈的回家了。(老王在火車站待了三天庐橙,其他啥事沒干假勿,還耗費(fèi)了6桶泡面一床棉被)
方式二:老王去到火車站售票大廳買票,沒買到态鳖,之后每天中午再去一次转培,終于在第三天買到了票。(老王往返車站6次浆竭,路上耗費(fèi)了3小時(shí)浸须,不過這幾天其他時(shí)間送了三天外賣,又給家里的老婆掙了不少錢)
方式三:老王去到火車站售票大廳買票邦泄,沒買到删窒,這時(shí)候看到一個(gè)黃牛在幫別人買票,老王想著還有外賣要送顺囊,就讓黃牛幫他買肌索,三天后黃牛買到了票通知他下班后來取。(老王往返車站兩次特碳,路上耗費(fèi)1小時(shí)诚亚,給了黃牛50塊手續(xù)費(fèi)晕换,其他時(shí)間送了三天外賣,由于老王臨近過年每天都沒耽擱的加班送外賣亡电,平臺(tái)獎(jiǎng)勵(lì)了老王500塊)
第一種方式就是阻塞IO模型届巩,第二種方式就是非阻塞IO模型,第三種方式就是IO復(fù)用模型了份乒。除了老王恕汇,老張老李......都找了黃牛買票,這樣大家都可以不用跑火車站了或辖,等黃牛消息就行瘾英。黃牛幫一個(gè)人買是買,幫多個(gè)人買也是買颂暇,反正都要在這里排隊(duì)缺谴,還能多掙幾份錢。老王老張老李的請(qǐng)求耳鸯,都復(fù)用這個(gè)黃牛搞定了湿蛔,老王他們節(jié)省了時(shí)間和精力干了其他事,黃牛一個(gè)人花費(fèi)了近乎一樣的時(shí)間和精力賺了多份錢县爬。 大概理解了I/O多路復(fù)用的概念接下來就看看在redis中是如何實(shí)現(xiàn)的阳啥。針對(duì)IO復(fù)用思想前后主要有select, poll, epoll 三種技術(shù)實(shí)現(xiàn)。
** Select:**select是I/O多路復(fù)用的第一個(gè)實(shí)現(xiàn)(1983年)财喳,有I/O事件發(fā)生了察迟,卻并不知道是哪幾個(gè)流,只能無差別輪詢所有流耳高,找出能讀出數(shù)據(jù)扎瓶,或者寫入數(shù)據(jù)的流,同時(shí)處理的流越多泌枪,輪詢時(shí)間就越長(zhǎng)概荷。就好比黃牛給多個(gè)人買票,但是并不知道買到票是誰的碌燕,只能不停的去問所有買票的人是不是你的乍赫。這樣買票的人越多,黃牛要問的人就越多陆蟆。
select本質(zhì)上是通過設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來進(jìn)行下一步處理。這樣所帶來的缺點(diǎn)是:
單個(gè)進(jìn)程可監(jiān)視的fd數(shù)量被限制惋增,即能監(jiān)聽端口的大小有限叠殷。一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看诈皿。32位機(jī)默認(rèn)是1024個(gè)林束。64位機(jī)默認(rèn)是2048.
對(duì)socket進(jìn)行掃描時(shí)是線性掃描像棘,即采用輪詢的方法,效率較低壶冒。
需要維護(hù)一個(gè)用來存放大量fd的數(shù)據(jù)結(jié)構(gòu)缕题,這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開銷大。
Poll:poll本質(zhì)上和select沒有區(qū)別胖腾,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間烟零,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷咸作,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備锨阿,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動(dòng)超時(shí)记罚,被喚醒后它又要再次遍歷fd墅诡。這個(gè)過程經(jīng)歷了多次無謂的遍歷。它沒有最大連接數(shù)的限制桐智,原因是它是基于鏈表來存儲(chǔ)的末早。
Epoll:epoll可以理解為event poll,不同于忙輪詢和無差別輪詢说庭,epoll會(huì)把哪個(gè)流發(fā)生了怎樣的I/O事件通知我們然磷。epoll實(shí)際上是事件驅(qū)動(dòng)(每個(gè)事件關(guān)聯(lián)上fd)的,此時(shí)我們對(duì)這些流的操作都是有意義的口渔。
通過以上三種技術(shù)實(shí)現(xiàn)的分析样屠,epoll無疑是最好的選擇,那么redis中是這樣選擇的嗎缺脉?先來看下redis在做多路復(fù)用函數(shù)選擇時(shí)的代碼實(shí)現(xiàn):
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
執(zhí)行邏輯如下圖:
可以看到redis針對(duì)不同的操作系統(tǒng)會(huì)選用不同的實(shí)現(xiàn)痪欲,主流操作系統(tǒng)都有類似epoll的實(shí)現(xiàn)作為選擇,同時(shí)也提供了select方式作為備選攻礼。
2.2 Reactor(反應(yīng)堆模式)
有了epoll等IO復(fù)用技術(shù)的支撐业踢,接下來我們看看redis是如何利用IO復(fù)用來串連起socket連接請(qǐng)求和具體任務(wù)處理的。
由上圖可以看出redis處理并發(fā)客戶端連接的方式是利用epoll來實(shí)現(xiàn)IO多路復(fù)用礁扮,將連接信息和事件放到隊(duì)列中知举,之后依次放到文件事件分派器,事件分派器將事件分發(fā)給事件處理器太伊。這種處理方式叫做反應(yīng)堆模式雇锡。
Redis是基于Reactor模式(反應(yīng)堆模式)開發(fā)了自己的網(wǎng)絡(luò)模型,形成了一個(gè)完備的基于IO復(fù)用的事件驅(qū)動(dòng)服務(wù)器僚焦。
上面我們了解到epoll方式的多路復(fù)用實(shí)現(xiàn)已經(jīng)是很高性能的了锰提,那么為什么redis在此基礎(chǔ)上還要基于Reactor來實(shí)現(xiàn)自己的網(wǎng)絡(luò)模型了?
epoll將收集到的可讀寫事件全部放入隊(duì)列中等待業(yè)務(wù)線程的處理,此時(shí)線程池的工作線程拿到任務(wù)進(jìn)行處理立肘,實(shí)際場(chǎng)景中可能有很多種請(qǐng)求類型边坤,工作線程每拿到一種任務(wù)就進(jìn)行相應(yīng)的處理,處理完成之后繼續(xù)處理其他類型的任務(wù)谅年,工作線程需要關(guān)注各種不同類型的請(qǐng)求茧痒,對(duì)于不同的請(qǐng)求選擇不同的處理方法,因此請(qǐng)求類型的增加會(huì)讓工作線程復(fù)雜度增加融蹂,維護(hù)起來也變得越來越困難旺订。
如果我們?cè)趀poll的基礎(chǔ)上進(jìn)行業(yè)務(wù)區(qū)分,并且對(duì)每一種業(yè)務(wù)設(shè)置相應(yīng)的處理函數(shù)殿较,每次來任務(wù)之后對(duì)任務(wù)進(jìn)行識(shí)別和分發(fā)耸峭,每種處理函數(shù)只處理一種業(yè)務(wù),這種模型也就是Reactor反應(yīng)堆模式的設(shè)計(jì)思路淋纲。
通俗點(diǎn)講就是黃牛的業(yè)務(wù)做的很好劳闹,找黃牛除了買火車票還有買機(jī)票電影票的,那么黃牛每次處理不同的業(yè)務(wù)的時(shí)候就要不斷跑來跑去切換業(yè)務(wù)場(chǎng)景洽瞬,顯然這樣業(yè)務(wù)沒法做大做強(qiáng)本涕,黃牛就找了多個(gè)業(yè)務(wù)員,負(fù)責(zé)專門買火車票伙窃,飛機(jī)票菩颖,電影票,這樣黃牛接到不同業(yè)務(wù)的時(shí)候就交給不同的業(yè)務(wù)員去做为障,接客能力一下就增強(qiáng)了晦闰。
三、告一段落
到這里我們從redis的線程模型分析了redis為什么使用單線程鳍怨,以及從單線程性能依舊很出色分析了基于I/O多路復(fù)用的反應(yīng)堆模式請(qǐng)求處理流程呻右。下一篇將從redis的內(nèi)存模型來解讀redisdb的數(shù)據(jù)結(jié)構(gòu)以及內(nèi)存回收機(jī)制。
關(guān)注IT巔峰技術(shù)鞋喇,私信作者声滥,獲取以下2021全球架構(gòu)師峰會(huì)PDF資料。