聊聊單線程的Redis為何會(huì)快到飛起

實(shí)際上它確實(shí)也很快 : ),但Redis底層卻是單線程的!有同學(xué)可能就要有疑問了,為什么單線程的Redis卻能夠快到飛起?
別急,我盡量用通俗易懂的語(yǔ)言來(lái)給各位說(shuō)道說(shuō)道~~
Redis是單線程,主要是指Redis的網(wǎng)絡(luò)IO和讀寫是由一個(gè)線程來(lái)完成的,但Redis的其他功能,比如持久化、異步刪除、集群數(shù)據(jù)同步等,其實(shí)是由額外的線程執(zhí)行的。這不是本文討論的重點(diǎn),有個(gè)印象即可
Redis為什么用單線程?
多線程的開銷
通常情況下,在采用多線程后,如果沒有良好的系統(tǒng)設(shè)計(jì),其實(shí)是右圖所展示的那樣(注意縱坐標(biāo))。剛開始增加線程數(shù)時(shí),系統(tǒng)吞吐率會(huì)增加,再進(jìn)一步增加線程時(shí),系統(tǒng)吞吐率就增長(zhǎng)遲緩了,甚至還會(huì)出現(xiàn)下降的情況。

關(guān)鍵瓶頸在于: 系統(tǒng)中通常會(huì)存在會(huì)被多線程同時(shí)訪問的共享資源,為了保證共享資源的正確性,就需要有額外的機(jī)制保證線程安全性,例如加鎖,這會(huì)帶來(lái)額外的開銷。
比如拿最常用的List類型來(lái)舉例吧,假設(shè)Redis采用多線程設(shè)計(jì),有兩個(gè)線程A和B分別對(duì)List做LPUSH和LPUSH操作,為了使得每次執(zhí)行都是相同的結(jié)果,即【B線程取出A線程放入的數(shù)據(jù)】就需要讓這兩個(gè)過(guò)程串行執(zhí)行。這就是多線程編程模式面臨的共享資源的并發(fā)訪問控制問題。

并發(fā)訪問控制一直是多線程開發(fā)中的一個(gè)難點(diǎn)問題:如果只是簡(jiǎn)單地采用一個(gè)互斥鎖,就會(huì)出現(xiàn)即使增加了線程,大部分線程也在等待獲取互斥鎖,并行變串行,系統(tǒng)吞吐率并沒有隨著線程的增加而增加。
同時(shí)加入并發(fā)訪問控制后也會(huì)降低系統(tǒng)代碼的可讀性和可維護(hù)性,所以Redis干脆直接采用了單線程模式。
Redis使用單線程為什么還這么快?
之所以使用單線程是Redis設(shè)計(jì)者多方面衡量的結(jié)果。
- Redis的大部分操作在內(nèi)存上完成
- 采用了高效的數(shù)據(jù)結(jié)構(gòu),例如哈希表和跳表
- 采用了多路復(fù)用機(jī)制,使其在網(wǎng)絡(luò)IO操作中能并發(fā)處理大量的客戶端請(qǐng)求,實(shí)現(xiàn)高吞吐率
既然Redis使用單線程進(jìn)行IO,如果線程被阻塞了就無(wú)法進(jìn)行多路復(fù)用了,所以不難想象,Redis肯定還針對(duì)網(wǎng)絡(luò)和IO操作的潛在阻塞點(diǎn)進(jìn)行了設(shè)計(jì)。
網(wǎng)絡(luò)與IO操作的潛在阻塞點(diǎn)
在網(wǎng)絡(luò)通信里,服務(wù)器為了處理一個(gè)Get請(qǐng)求,需要監(jiān)聽客戶端請(qǐng)求(bind/listen),和客戶端建立連接(accept),從socket中讀取請(qǐng)求(recv),解析客戶端發(fā)送請(qǐng)求(parse),最后給客戶端返回結(jié)果(send)。
最基本的一種單線程實(shí)現(xiàn)是依次執(zhí)行上面的操作。

上面標(biāo)紅的accept和recv操作都是潛在的阻塞點(diǎn):
- 當(dāng)Redis監(jiān)聽到有連接請(qǐng)求,但卻一直不能成功建立起連接時(shí),就會(huì)阻塞在
accept()函數(shù)這里,其他客戶端此時(shí)也無(wú)法和Redis建立連接 - 當(dāng)Redis通過(guò)
recv()從一個(gè)客戶端讀取數(shù)據(jù)時(shí),如果數(shù)據(jù)一直沒有到達(dá),也會(huì)一直阻塞
基于多路復(fù)用的高性能IO模型
為了解決IO中的阻塞問題,Redis采用了Linux的IO多路復(fù)用機(jī)制,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽套接字和已連接套接字(select/epoll)。
內(nèi)核會(huì)一直監(jiān)聽這些套接字上的連接或數(shù)據(jù)請(qǐng)求。一旦有請(qǐng)求到達(dá),就會(huì)交給Redis處理,這就實(shí)現(xiàn)了一個(gè)Redis線程處理多個(gè)IO流的效果。

此時(shí),Redis線程就不會(huì)阻塞在某一個(gè)特定的客戶端請(qǐng)求處理上,所以它可以同時(shí)和多個(gè)客戶端連接并處理請(qǐng)求。
回調(diào)機(jī)制
select/epoll一旦監(jiān)測(cè)到FD上有請(qǐng)求到達(dá)時(shí),就會(huì)觸發(fā)相應(yīng)的事件被放進(jìn)一個(gè)隊(duì)列里,Redis線程對(duì)該事件隊(duì)列不斷進(jìn)行處理,所以就實(shí)現(xiàn)了基于事件的回調(diào)。
例如,Redis會(huì)對(duì)Accept和Read事件注冊(cè)accept和get回調(diào)函數(shù)。當(dāng)Linux內(nèi)核監(jiān)聽到有連接請(qǐng)求或讀數(shù)據(jù)請(qǐng)求時(shí),就會(huì)觸發(fā)Accept事件和Read事件,此時(shí),內(nèi)核就會(huì)回調(diào)Redis相應(yīng)的accept和get函數(shù)進(jìn)行處理。
Redis的性能瓶頸點(diǎn)
經(jīng)過(guò)上面的分析,雖然通過(guò)多路復(fù)用機(jī)制可以同時(shí)監(jiān)聽多個(gè)客戶端的請(qǐng)求,但Redis仍然有一些性能瓶頸點(diǎn),這也是我們平時(shí)編程需要極力避免的情況。
1. 耗時(shí)操作
任意一個(gè)請(qǐng)求在Redis中一旦耗時(shí)較久,都會(huì)影響整個(gè)server的性能。后面的請(qǐng)求都要等前面這個(gè)耗時(shí)請(qǐng)求處理完成,自己才能被處理到。
這一點(diǎn)需要我們?cè)谠O(shè)計(jì)業(yè)務(wù)場(chǎng)景時(shí)去規(guī)避;Redis的lazy-free機(jī)制也把釋放內(nèi)存的耗時(shí)操作放在了異步線程中去執(zhí)行了。
2. 高并發(fā)場(chǎng)景
并發(fā)量非常大時(shí),單線程讀寫客戶端IO數(shù)據(jù)存在性能瓶頸,雖然采用IO多路復(fù)用機(jī)制,但還是只能單線程依次讀取客戶端的數(shù)據(jù),無(wú)法利用到CPU多核。
Redis在6.0可以利用CPU多核多線程讀寫客戶端數(shù)據(jù),但只是針對(duì)客戶端的讀寫是并行的,每個(gè)命令的真正操作還是單線程。
其他Redis相關(guān)的有趣問題
借此機(jī)會(huì)也提幾個(gè)和redis相關(guān)的有意思的問題。

- 為什么要用Redis,直接訪問內(nèi)存不好嗎?
這一條其實(shí)并沒有很明確的界定,對(duì)于一些不經(jīng)常變動(dòng)的數(shù)據(jù),可以直接放到內(nèi)存里,不一定要放到Redis里,可以放到內(nèi)存里。一致性問題:如果一個(gè)數(shù)據(jù)被修改了,數(shù)據(jù)在本地內(nèi)存里的話,可能只有一臺(tái)服務(wù)器上的數(shù)據(jù)被修改了。如果用Redis里面的話,我們?cè)L問Redis服務(wù)器,可以解決一致性問題。
- 數(shù)據(jù)太多內(nèi)存放不下怎么辦?比如我要緩存100G的數(shù)據(jù),怎么辦?
這里也要打一個(gè)廣告Tair是淘寶開源的分布式KV緩存系統(tǒng),它從Redis繼承了豐富的操作,理論上總數(shù)據(jù)量無(wú)限制,針對(duì)可用性、可擴(kuò)展性、可靠性也進(jìn)行了升級(jí),感興趣的小伙伴們可以了解一下~
到此這篇關(guān)于聊聊單線程的Redis為何會(huì)快到飛起的文章就介紹到這了,更多相關(guān)Java Redis內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
http basic authentication通過(guò)post方式訪問api示例分享 basic認(rèn)證示例
在HTTP中,基本認(rèn)證是一種用來(lái)允許Web瀏覽器或其他客戶端程序在請(qǐng)求時(shí)提供以用戶名和口令形式的憑證,這篇文章主要介紹了http basic authentication通過(guò)post方式訪問api示例,大家參考使用吧2014-01-01
淺談SpringBoot如何封裝統(tǒng)一響應(yīng)體
今天帶各位小伙伴學(xué)習(xí)SpringBoot如何封裝統(tǒng)一響應(yīng)體,文中有非常詳細(xì)的介紹及代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-05-05
淺談Java讀寫注冊(cè)表的方式Preferences與jRegistry
這篇文章主要介紹了淺談Java讀寫注冊(cè)表的方式Preferences與jRegistry,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02
Java String不可變性實(shí)現(xiàn)原理解析
這篇文章主要介紹了Java String不可變性實(shí)現(xiàn)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
SpringBoot實(shí)現(xiàn)過(guò)濾器Filter的三種方式
過(guò)濾器Filter由Servlet提供,基于函數(shù)回調(diào)實(shí)現(xiàn)鏈?zhǔn)綄?duì)網(wǎng)絡(luò)請(qǐng)求與響應(yīng)的攔截與修改,本文講給大家詳細(xì)介紹SpringBoot實(shí)現(xiàn)過(guò)濾器Filter的三種方式,需要的朋友可以參考下2023-08-08
Spring三級(jí)緩存解決循環(huán)依賴的過(guò)程分析
這篇文章主要介紹了Spring三級(jí)緩存解決循環(huán)依賴,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04

