目錄
面試題
Redis到底是多線程還是單線程?
簡單回答
詳解
Redis的“單線程”
Redis為什么選擇單線程?
后來Redis為什么又逐漸加入了多線程特性?
Redis為什么快?
回答
IO多路復(fù)用
Unix網(wǎng)絡(luò)編程的5種IO模型
主線程和IO線程怎么協(xié)作完成請求處理的
四個(gè)階段
淺談IO多路復(fù)用
文件描述符(FileDescriptor, FD)
什么是IO多路復(fù)用
場景模擬
總結(jié)
Redis 7 默認(rèn)設(shè)置是否開啟了多線程?
總結(jié)
這篇我們從幾個(gè)面試題入手
面試題
Redis到底是多線程還是單線程?
簡單回答
Redis 是從4開始慢慢支持多線程的,直到 Redis6/7 后才穩(wěn)定
詳解
這種問法其實(shí)并不嚴(yán)謹(jǐn),單線程還是多線程需要視版本而定。
Redis的版本很多3.X、4.X、6.X,版本不同架構(gòu)也是不同的,不限定版本問是否單線程也不太嚴(yán)謹(jǐn)。
版本3.×(最早版本),也就是大家口口相傳的Redis是單線程;
版本4.×,嚴(yán)格意義來說也不是單線程,而是負(fù)責(zé)處理客戶端請求的線程單線程,但是開始加了點(diǎn)多線程的東西(異步刪除);
版本6.x 開始,全面支持多線程。
Redis的“單線程”
Redis的“單線程”主要是指Redis的網(wǎng)絡(luò)IO和鍵值對讀寫是由一個(gè)線程來完成的,Redis在處理客戶端的請求時(shí)包括獲取(socket讀)、解析、執(zhí)行、內(nèi)容返回(socket寫)等都由一個(gè)順序串行的主線程處理(第一篇介紹過的那些原子的命令),這也是Redis對外提供鍵值存儲服務(wù)的主要流程。
Redis采用Reactor模式的網(wǎng)絡(luò)模型,對于一個(gè)客戶端請求,主線程負(fù)責(zé)一個(gè)完整的處理過程,如下圖:
補(bǔ)充:
Reactor 模式是一種并發(fā)模型,在這種模型中,主線程(Reactor)避免了由于等待一或多個(gè)并發(fā)事件(比如 I/O 操作)的完成而無法繼續(xù)工作的阻塞。通過使用非阻塞 I/O 操作和事件通知,主線程在并發(fā)操作完成時(shí)得到通知。
一般來說,Reactor 模式的工作流程如下:
- 應(yīng)用程序?qū)⑿枰?strong>監(jiān)聽的 I/O 事件(比如 socket 的可讀、可寫事件)注冊到 Reactor 中,并且關(guān)聯(lián)對應(yīng)的處理事件。
- Reactor 不斷地輪詢這些事件,當(dāng)某個(gè)事件到達(dá)的時(shí)候(比如 socket 中有數(shù)據(jù)可讀),Reactor 將這個(gè)事件對應(yīng)的處理事件喚醒,交由一個(gè)工作線程(或者稱之為 EventHandler)去處理。
- 工作線程處理完相應(yīng)的事件后,通知 Reactor 繼續(xù)監(jiān)聽這個(gè)事件。
這種模型非常適合于大并發(fā),少邏輯的網(wǎng)絡(luò)程序中,比如 Nginx 中就用到了這種設(shè)計(jì)模式。通過這種方式,一個(gè)線程可以處理很多連接的事件,而不需要為每個(gè)連接都創(chuàng)建一個(gè)線程,這樣就可以避免線程切換的開銷,并且可以更有效地使用系統(tǒng)資源。
但Redis的其他功能,比如持久化RDB、AOF、異步刪除、集群同步數(shù)據(jù)等都是由額外的線程執(zhí)行的,因此整個(gè)Redis可以看作是多線程的。
Redis為什么選擇單線程?
準(zhǔn)確的說,應(yīng)該是Redis 4.0之前一直采用單線程
主要原因有:
- 使用單線程模型使Redis的開發(fā)和維護(hù)更簡單
- 雖然使用的是單線程,但也可以并發(fā)處理多客戶端的請求(IO多路復(fù)用和非阻塞IO)
- 對于Redis系統(tǒng)來說,主要的性能瓶頸是內(nèi)存/網(wǎng)絡(luò)帶寬,而非CPU
后來Redis為什么又逐漸加入了多線程特性?
- 硬件的發(fā)展
-
- CPU并不是Redis的瓶頸,通常Redis要不受內(nèi)存限制要不受網(wǎng)絡(luò)限制,但是隨著計(jì)算機(jī)硬件的發(fā)展,多核CPU已經(jīng)成為常態(tài),為了更大限度的利用CPU
- 單線程的缺點(diǎn)
-
- Redis使用單線程也是有一定缺點(diǎn)的,比較典型的就是使用del指令刪除大key數(shù)據(jù)時(shí)(比如包含了成千上萬個(gè)元素的hash集合,關(guān)于大key的問題我們會在Redis系列的下一篇專門介紹,這里先簡單舉個(gè)例子),del指令就會造成主線程卡頓。
- 由于Redis3.x完全是單線程的,del指令刪除時(shí)會等待??很久才釋放,如果再加上高并發(fā)場景(? ̄? ??  ̄??)額。。這就是Redis3.x單線程時(shí)代最經(jīng)典的故障,大key刪除的頭疼問題,于是Redis4.0就新增了多線程模塊,也主要是為了解決這個(gè)問題。
- 此外例如flushdb還有flushall在數(shù)據(jù)量達(dá)到一定程度時(shí),也會造成卡頓,因此一開始Redis就是把某些時(shí)間復(fù)雜度高,占主線程CPU時(shí)間片較高,造成主線程卡頓的操作使用多線程來處理(bio子線程),以減少主線程阻塞時(shí)間,從而減少那個(gè)諸如del操作導(dǎo)致性能和穩(wěn)定性的問題。
總的來說,就是與時(shí)俱進(jìn)( ̄? ̄)/??~~~
Redis為什么快?
回答
Redis 3.0 單線程時(shí)代依舊很快的原因:
- 基于內(nèi)存操作:Rdis的所有數(shù)據(jù)都存在內(nèi)存中,因此所有的運(yùn)算都是內(nèi)存級別的,所以他的性能比較高
- 數(shù)據(jù)結(jié)構(gòu)簡單:Rdis的數(shù)據(jù)結(jié)構(gòu)是專門設(shè)計(jì)的,而這些簡單的數(shù)據(jù)結(jié)構(gòu)的查找和操作的時(shí)間大部分復(fù)雜度都是O(1),因此性能比較高
- 多路復(fù)用和非阻塞I/O:Redis使用多路復(fù)用功能來監(jiān)聽多個(gè)socket連接客戶端,這樣就可以使用一個(gè)線程連接來處理多個(gè)請求,減少線程切換帶來的
- 避免上下文切換:因?yàn)槭菃尉€程模型,因此就避免了不必要的上下文切換和多線程競爭,這就省去了多線程切換帶來的時(shí)間和性能上的消耗,而且單線程
現(xiàn)在Redis快的原因
在上面原因的基礎(chǔ)上
- IO多路復(fù)用 + epoll函數(shù)使用
接下來我們詳細(xì)介紹下IO多路復(fù)用
IO多路復(fù)用
IO多路復(fù)用(IO multiplexing)是Unix網(wǎng)絡(luò)編程的5種IO模型之一。
Unix網(wǎng)絡(luò)編程的5種IO模型
- Blocking IO 阻塞IO
- NoneBlocking IO 非阻塞IO
- IO multiplexing IO 多路復(fù)用
- signal driven IO 信號驅(qū)動(dòng)IO
- asynchronous IO 異步IO
Redis一直被大家熟知的就是它的單線程架構(gòu),雖然從Redis4.0開始使用了多線程,也是為了處理數(shù)據(jù)刪除、快照刪除等耗時(shí)操作,從網(wǎng)絡(luò)IO處理到實(shí)際的讀寫命令處理都是由主線程獨(dú)自處理的(心疼下主線程)。
在Redis 6/7中,Redis全面支持了多線程。這是由于隨著硬件性能的提升,Redis的性能瓶頸主要出現(xiàn)在網(wǎng)絡(luò)IO上,就是完全靠單個(gè)主線程處理網(wǎng)絡(luò)請求的速度跟不上底層網(wǎng)絡(luò)硬件的速度,于是采用多個(gè)線程處理網(wǎng)絡(luò)IO,提高網(wǎng)絡(luò)請求處理的并行度。
但是,Redis的多IO線程只是用來處理網(wǎng)絡(luò)請求的,對于讀寫操作命令 Redis 仍然使用單線程來處理。這是因?yàn)?,Redis處理請求時(shí),網(wǎng)絡(luò)處理經(jīng)常是瓶頸,通過多個(gè)IO線程并行處理網(wǎng)絡(luò)操作,可以提升實(shí)例的整體處理性能。而繼續(xù)使用單線程執(zhí)行命令操作,就不用為了保證Lua腳本、事務(wù)的原子性,額外開發(fā)多線程互斥加鎖機(jī)制了(不管加鎖操作處理),這樣一來,Redis 線程模型實(shí)現(xiàn)就簡單了。
主線程和IO線程怎么協(xié)作完成請求處理的
四個(gè)階段
- 階段一:服務(wù)端和客戶端建立Socket連接,并分配處理線程 首先,主線程負(fù)責(zé)接收建立連接請求,當(dāng)有客戶端請求和實(shí)例建立Socket連接時(shí),主線程會創(chuàng)建和客戶端的連接,并把Socket放入全局等待隊(duì)列中,緊接著,主線程通過輪詢方法把Socket連接分配給IO線程。
- 階設(shè)二:IO線程讀取并解折請求 主線程一旦把Socket分配給IO線程,就會進(jìn)入阻塞狀態(tài),等待IO線程完成客戶端請求的讀取和解析,因?yàn)橛卸鄠€(gè)IO線程在并行處理,所以,這個(gè)過程很快就可以完成。
- 階段三:主線程執(zhí)行請求操作 等到IO線程解析完請求,主線程還是會以單線程的方式執(zhí)行這些命令操作。
- 階設(shè)四:IO線程回寫Socket和主線程清空全局隊(duì)列 當(dāng)主線程執(zhí)行完請求操作后,會把需要返回的結(jié)果寫入緩沖區(qū),然后,主線程會阻塞等待IO線程,把這些結(jié)果回寫到Socket中,并返回給客戶端。和IO線程讀取和解析請求一樣,IO線程回寫Socket時(shí),也是有多個(gè)線程在并發(fā)執(zhí)行,所以回寫Socket的速度也很快。等到IO線程回寫Socket完畢,主線程會清空全局隊(duì)列,等待客戶端的后續(xù)請求。
淺談IO多路復(fù)用
由于IO多路復(fù)用(IO multiplexing)是Unix網(wǎng)絡(luò)編程的5種IO模型之一,我們先介紹下Linux的相關(guān)內(nèi)容,便于理解(熟悉的人可以直接跳到下一趴)。
在Linux中,一切皆文件,這就要提到“文件描述符”這個(gè)概念了。
文件描述符(FileDescriptor, FD)
文件描述符是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個(gè)非負(fù)整數(shù),實(shí)際上它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向該進(jìn)程返回一個(gè)文件描述符。
在程序設(shè)計(jì)中,文件描述符這一概念往往只適用于UNIX或者Linux這樣的操作系統(tǒng)。
什么是IO多路復(fù)用
一種同步的IO模型,實(shí)現(xiàn)一個(gè)線程監(jiān)視多個(gè)文件句柄,一旦某個(gè)文件句柄就緒,就能通知到對應(yīng)程序進(jìn)行相應(yīng)的讀寫操作,沒有文件句柄就緒時(shí),就會阻塞應(yīng)用程序,從而釋放CPU資源。
我們來介紹幾個(gè)概念:
- I/O
-
- 網(wǎng)絡(luò)I/O,尤其在操作系統(tǒng)層面指數(shù)據(jù)在內(nèi)核態(tài)和用戶態(tài)之間的讀寫操作
- 多路
-
- 多個(gè)客戶端連接(連接就是套接字描述符,即socket或者channel)
- 復(fù)用
-
- 復(fù)用一個(gè)或者幾個(gè)線程連接
- IO多路復(fù)用
-
- 也就是說一個(gè)或者一組線程處理多個(gè)TCP,使用單進(jìn)程就能實(shí)現(xiàn)同時(shí)處理多個(gè)客戶端的連接,無需創(chuàng)建或者維護(hù)過多的進(jìn)程/線程
總結(jié)
- 一個(gè)服務(wù)端進(jìn)程可以同時(shí)處理多個(gè)套接字描述符
- 實(shí)現(xiàn)多路復(fù)用的模型有3種:可以分select->poll->epoll3個(gè)階段來描述
場景模擬
不知道大家有沒有注意過麻辣燙店是怎么做煮麻辣燙的,他們可不是一碗一碗單獨(dú)的鍋單獨(dú)的灶煮的(太費(fèi)??),一般是有一個(gè)方形大鍋,整個(gè)鍋里加滿水,下面一個(gè)灶加熱,大鍋里分成一個(gè)個(gè)小格子(就類似重慶火鍋那種九宮格),每個(gè)小格子里煮一個(gè)客人選的菜。
基本流程就是,客人選完菜去收銀那里稱斤付款(人多則需要排隊(duì)),服務(wù)員每收到一個(gè)客人選的菜,就把它們倒進(jìn)一個(gè)小格子里煮,然后再回來收下一位的……還會時(shí)不時(shí)看看哪個(gè)小格子里的菜熟了,就把它盛到碗里,端回給客人
我們根據(jù)模擬情景來簡單概括下IO多路復(fù)用模型:
將用戶Socket對應(yīng)的文件描述符(FileDescriptor)注冊進(jìn)epoll,然后epoll幫你監(jiān)聽哪些Socket上有消息到達(dá),這樣就避免了大量的無用操作,此時(shí)的Socket應(yīng)該采用非阻塞模式,這樣整個(gè)過程只在調(diào)用select、poll、epoll這些函數(shù)是才會阻塞,收發(fā)客戶消息是不會阻塞的,整個(gè)進(jìn)程/線程就會被充分利用起來,這就是事件驅(qū)動(dòng),所謂的reactor反應(yīng)模式。
在單個(gè)線程通過記錄跟蹤每一個(gè)Socket(I/O流)的狀態(tài)來同時(shí)管理多個(gè)I/O流,一個(gè)服務(wù)端進(jìn)程可以同時(shí)處理多個(gè)套接字描述符,以此來提高服務(wù)器的吞吐能力。
大家都用過nginx,nginx使用epoll接收請求,ngnix會有很多鏈接進(jìn)來,epoll會把他們都監(jiān)視起來,然后像撥開關(guān)一樣,誰有數(shù)據(jù)就撥向誰,然后調(diào)用相應(yīng)的代碼處理。Redis類似同理,這就是IO多路復(fù)用原理,有請求就響應(yīng),沒請求不打擾。
IO多路復(fù)用達(dá)到的效果就是只使用一個(gè)服務(wù)端進(jìn)程可以同時(shí)處理多個(gè)套接字描述符連接。
客戶端請求服務(wù)端時(shí),實(shí)際就是在服務(wù)端的Socket文件中寫入客戶端對應(yīng)的文件描述符(FileDescriptor),如果有多個(gè)客戶端同時(shí)請求服務(wù)端,為每次請求分配一個(gè)線程,類似每次來就new一個(gè)太耗費(fèi)服務(wù)端資源,因此,只使用一個(gè)線程來監(jiān)聽多個(gè)文件描述符,即IO多路復(fù)用。
采用IO多路復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請求,一個(gè)服務(wù)端進(jìn)程可以同時(shí)處理多個(gè)套接字描述符。
從Rdis6開始,就新增了多線程的功能來提高I/O的讀寫性能,他的主要實(shí)現(xiàn)思路是將主線程的IO讀寫任務(wù)拆分給一組獨(dú)立的線程去執(zhí)行,這樣就可以使多個(gè)Socket的讀寫可以并行化了,采用多路I/O復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請求(盡量減少網(wǎng)絡(luò)IO的時(shí)間消耗),將最耗時(shí)的Socket的讀取、請求解析、寫入單獨(dú)外包出去,剩下的命令執(zhí)行仍然由主線程串行執(zhí)行并和內(nèi)存的數(shù)據(jù)交互。
于是,網(wǎng)絡(luò)IO操作就變成多線程了,其他核心操作仍然是主線程單獨(dú)處理(線程安全)
總結(jié)
- Redis的工作線程是單線程的,但是整個(gè)Redis是多線程的
- Redis6/7將網(wǎng)絡(luò)數(shù)據(jù)讀寫、請求協(xié)議解析通過多個(gè)IO線程來處理,真正執(zhí)行命令的線程仍然是主線程單獨(dú)進(jìn)行操作(單線程),一舉兩得,既解決網(wǎng)絡(luò)IO問題(多個(gè)IO線程),有保證了線程安全且處理方式簡單(單線程執(zhí)行命令)
Redis 7 默認(rèn)設(shè)置是否開啟了多線程?
如果你在實(shí)際應(yīng)用中,發(fā)現(xiàn)Redis實(shí)例的CPU開銷不大但吞吐量卻沒有提升,可以考慮使用Reds7的多線程機(jī)制(默認(rèn)關(guān)閉,需手動(dòng)開啟),加速網(wǎng)絡(luò)處理,進(jìn)而提升實(shí)例的吞吐量。
Redis7將所有的數(shù)據(jù)放在內(nèi)存中,內(nèi)存的響應(yīng)時(shí)長大約為100納秒,對于小數(shù)據(jù),Redis服務(wù)器可以處理8W到10W的QPS(實(shí)驗(yàn)室數(shù)據(jù),極限),但是對于大部分的公司已經(jīng)夠用了,所以在Redis6.0以后,多線程機(jī)制默認(rèn)是關(guān)閉的,如果需要使用,則要在redis.conf配置文件中修改,主要改兩個(gè)地方:文章來源:http://www.zghlxwxcb.cn/news/detail-610540.html
- 設(shè)置io-threads-do-redis配置項(xiàng)為yes,表示啟動(dòng)多線程
- 設(shè)置線程個(gè)數(shù),io-threads 個(gè)數(shù),關(guān)于個(gè)數(shù)的設(shè)置,官方的建議是如果為4核的CPU,建議線程數(shù)設(shè)置為2或3,如果為8核CPU建議線程數(shù)設(shè)置為6(線程數(shù)一定要小于機(jī)器核數(shù),并非越大越好)
總結(jié)
Redis基于內(nèi)存操作、數(shù)據(jù)結(jié)構(gòu)簡單、多路復(fù)用和非阻塞I/O等特性,避免了不必要的上下文切換,在單線程的環(huán)境下依舊很快。但對于大數(shù)據(jù)的key刪除還是會卡頓,因此在4.0版本中引入了多線程unlink key/flushall async等命令,主要用于Redis數(shù)據(jù)的異步刪除。而在Redis6/7中引入了I/O多線程的讀寫,增加了吞吐量,而命令的執(zhí)行依舊是有主線程串行執(zhí)行的,因此在多線程下操作Redis既能保持良好的性能和響應(yīng)速度,并且不會出現(xiàn)線程安全的問題。文章來源地址http://www.zghlxwxcb.cn/news/detail-610540.html
到了這里,關(guān)于【Redis】高級篇: 一篇文章講清楚Redis的單線程和多線程的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!