0. 背景
Redis作為一個(gè)基于內(nèi)存的緩存系統(tǒng),一直以高性能著稱,
在單線程處理情況下,讀速度可達(dá)到11萬次/s,寫速度達(dá)到8.1萬次/s。
Redis6.0之前為什么一直不使用多線程?
官方曾做過類似問題的回復(fù):使用Redis時(shí),幾乎不存在CPU成為瓶頸的情況, Redis主要受限于內(nèi)存和網(wǎng)絡(luò)。
但是,單線程的設(shè)計(jì)也給Redis帶來一些問題:
- 只能使用CPU一個(gè)核
- 如果刪除的鍵過大(eg: Set類型中有上百萬個(gè)對象),會導(dǎo)致服務(wù)端阻塞好幾秒
- QPS難再提高
針對上面問題,Redis在4.0版本以及6.0版本分別引入了Lazy Free以及多線程IO,逐步向多線程過渡。
?
1. Redis單線程架構(gòu)原理
Redis單線程是如何支持客戶端并發(fā)請求的呢?
Redis服務(wù)器是一個(gè)事件驅(qū)動程序,服務(wù)器需要處理以下兩類事件:
- 文件事件
Redis服務(wù)器通過套接字與客戶端(或者其他Redis服務(wù)器)進(jìn)行連接。
文件事件就是服務(wù)器對套接字操作的抽象。
服務(wù)器與客戶端的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作。
(eg: 連接accept,read,write,close等)
- 時(shí)間事件
Redis服務(wù)器中的一些操作(eg: serverCron函數(shù))需要在給定的時(shí)間點(diǎn)執(zhí)行。
時(shí)間事件就是服務(wù)器對這類定時(shí)操作的抽象
(eg: 過期鍵清理,服務(wù)狀態(tài)統(tǒng)計(jì)等)
Redis將文件事件和時(shí)間事件進(jìn)行抽象,時(shí)間輪詢器會監(jiān)聽I/O事件表:
一旦有文件事件就緒,Redis就會優(yōu)先處理文件事件,
接著處理時(shí)間事件。
在上述所有事件處理上,Redis都是以單線程形式處理,所以說Redis是單線程的。
處理過程見下圖

Redis基于Reactor模式開發(fā)了自己的I/O事件處理器,也就是文件事件處理器。
Redis在I/O事件處理上,采用了I/O多路復(fù)用技術(shù),同時(shí)監(jiān)聽多個(gè)套接字,
并為套接字關(guān)聯(lián)不同的事件處理函數(shù),通過一個(gè)線程實(shí)現(xiàn)了多客戶端并發(fā)處理。
處理過程見下圖

上述的設(shè)計(jì),在數(shù)據(jù)處理上避免了加鎖操作,既使得實(shí)現(xiàn)上足夠簡潔,也保證了其高性能。
當(dāng)然,Redis單線程只是指其在事件處理上,實(shí)際上,Redis也并不是單線程的,比如生成RDB文件,就會fork一個(gè)子進(jìn)程來實(shí)現(xiàn)。
?
2. Redis 4.0的Lazy Free機(jī)制
背景:
客戶端向Redis發(fā)送一條耗時(shí)較長的命令,比如刪除一個(gè)含有上百萬對象的Set鍵,或者執(zhí)行flushdb,flushall操作,
Redis服務(wù)器需要回收大量的內(nèi)存空間,導(dǎo)致服務(wù)器卡住好幾秒,對負(fù)載較高的緩存系統(tǒng)而言將會是個(gè)災(zāi)難。
為了解決這個(gè)問題,在Redis 4.0版本引入了Lazy Free,將慢操作異步化,這也是在事件處理上向多線程邁進(jìn)了一步。
將大鍵的刪除操作異步化,采用非阻塞刪除(對應(yīng)命令UNLINK)。
大鍵的空間回收交由單獨(dú)線程實(shí)現(xiàn),主線程只做關(guān)系解除,可以快速返回,繼續(xù)處理其他事件,避免服務(wù)器長時(shí)間阻塞。
意義:
Redis在4.0版本引入了Lazy Free,自此Redis有了一個(gè)Lazy Free線程專門用于大鍵的回收。
同時(shí),也去掉了聚合類型的共享對象,這為多線程帶來可能。
這為Redis在6.0版本實(shí)現(xiàn)了多線程I/O打下了基礎(chǔ)。
?
3. Redis 6.0多線程實(shí)現(xiàn)機(jī)制
Redis 6.0的多線程并未將事件處理改成多線程,而是在I/O上。
因?yàn)?,如果把事件處理改成多線程,不但會導(dǎo)致鎖競爭,而且會有頻繁的上下文切換,
即使用分段鎖來減少競爭,對Redis內(nèi)核也會有較大改動,性能也不一定有明顯提升。
流程簡述如下:
1、主線程負(fù)責(zé)接收建立連接請求,獲取 socket 放入全局等待讀處理隊(duì)列
2、主線程處理完讀事件之后,通過 RR(Round Robin) 將這些連接分配給這些 IO 線程
3、主線程阻塞等待 IO 線程讀取 socket 完畢
4、主線程通過單線程的方式執(zhí)行請求命令,請求數(shù)據(jù)讀取并解析完成,但并不執(zhí)行
5、主線程阻塞等待 IO 線程將數(shù)據(jù)回寫 socket 完畢
6、解除綁定,清空等待隊(duì)列
見下圖

?
4. Redis6.0多線程的設(shè)置
Redis6.0的多線程默認(rèn)是禁用的,只使用主線程。
如需開啟需要修改redis.conf配置文件:
io-threads-do-reads yes
開啟多線程后,還需要設(shè)置線程數(shù),否則是不生效的。
同樣修改redis.conf配置文件:
io-threads 4