1. 基礎知識
Linux 文件描述符
文件描述符(File descriptor)是計算機科學中的一個術(shù)語,是一個用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數(shù)。實際上,它是一個索引值,指向內(nèi)核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。
用戶空間 / 內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器,那么對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。
操作系統(tǒng)的核心是內(nèi)核,獨立于普通的應用程序,可以訪問受保護的內(nèi)存空間,也有訪問底層硬件設備的所有權(quán)限。為了保證用戶進程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。
緩存I/O
緩存I/O又稱為標準I/O,大多數(shù)文件系統(tǒng)的默認I/O操作都是緩存I/O。在Linux的緩存I/O機制中,操作系統(tǒng)會將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,即數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應用程序的地址空間。
2. Unix中的5種IO技術(shù)
[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路復用
[4] signal driven IO - 信號驅(qū)動IO
[5] asynchronous IO - 異步IO
其中前面4種IO都可以歸類為synchronous IO - 同步IO,而select、poll、epoll本質(zhì)上也都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
需要注意的是,除了異步 IO 外,其它的 I/O 模型其實都可以歸類為阻塞式 I/O 模型。
不同的是像阻塞式 I/O 模型在第一階段讀取數(shù)據(jù)的時候,如果此時數(shù)據(jù)未準備就緒需要阻塞,在第二階段數(shù)據(jù)準備就緒后需要將數(shù)據(jù)從內(nèi)核態(tài)復制到用戶態(tài)這一步也是阻塞的。
而多路復用 IO 模型在第一階段是不阻塞的,只會在第二階段阻塞
3. Redis為什么快?
Redis的單線程
我們通常說,Redis 是單線程,主要是指 Redis 的網(wǎng)絡 IO 和鍵值對讀寫是由一個線程來完成的,這也是 Redis 對外提供鍵值存儲服務的主要流程。但 Redis 的其他功能,比如持久化、異步刪除、集群數(shù)據(jù)同步等,其實是由額外的線程執(zhí)行的。
Redis 為什么采用單線程模式
Redis 有 List 的數(shù)據(jù)類型,并提供出隊(LPOP)和入隊(LPUSH)操作。假設 Redis 采用多線程設計?,F(xiàn)在有兩個線程 A 和 B,線程 A 對一個 List 做 LPUSH 操作,并對隊列長度加 1。同時,線程 B 對該 List 執(zhí)行 LPOP 操作,并對隊列長度減 1。為了保證隊列長度的正確性,Redis 需要讓線程 A 和 B 的 LPUSH 和 LPOP 串行執(zhí)行,這樣一來,Redis 可以無誤地記錄它們對 List 長度的修改。否則,我們可能就會得到錯誤的長度結(jié)果。這就是多線程編程模式面臨的共享資源的并發(fā)訪問控制問題。
采用多線程開發(fā)一般會引入同步原語來保護共享資源的并發(fā)訪問,這也會降低系統(tǒng)代碼的易調(diào)試性和可維護性。為了避免這些問題,Redis 直接采用了單線程模式。
單線程Redis為什么快
- 純內(nèi)存操作
- 單線程,避免了不必要的上下文切換和競爭條件
- I/O多路復用,如Linux下的poll,epoll,select多路復用是指使用一個線程來檢查多個文件符FD(socket,文件,管道)的就緒狀態(tài);比如select和poll函數(shù),傳入多個文件描述符,一個文件符就緒,則返回,否則阻塞直到超時。
網(wǎng)絡I/O
網(wǎng)絡IO處理函數(shù)包括bind/listen、accept、recv、parse 和 send,其中accept() 和 recv()是潛在的阻塞點,當 Redis 監(jiān)聽到一個客戶端有連接請求,但一直未能成功建立起連接時,會阻塞在 accept() 函數(shù)這里,導致其他客戶端無法和 Redis 建立連接。類似的,當 Redis 通過 recv() 從一個客戶端讀取數(shù)據(jù)時,如果數(shù)據(jù)一直沒有到達,Redis 也會一直阻塞在 recv()。
非阻塞模式
Socket 網(wǎng)絡模型的非阻塞模式設置,主要體現(xiàn)在三個關(guān)鍵的函數(shù)調(diào)用上。
在 socket 模型中,不同操作調(diào)用后會返回不同的套接字類型。socket() 方法會返回主動套接字,然后調(diào)用 listen() 方法,將主動套接字轉(zhuǎn)化為監(jiān)聽套接字,此時,可以監(jiān)聽來自客戶端的連接請求。最后,調(diào)用 accept() 方法接收到達的客戶端連接,并返回已連接套接字。
針對監(jiān)聽套接字,我們可以設置非阻塞模式:當Redis 調(diào)用 accept() 但一直未有連接請求到達時,Redis 線程可以返回處理其他操作,而不用一直等待。但是,你要注意的是,調(diào)用 accept() 時,已經(jīng)存在監(jiān)聽套接字了。

Redis中的I/O多路復用
Linux 中的 IO 多路復用機制是指一個線程處理多個 IO 流,就是我們經(jīng)常聽到的 select/epoll 機制。簡單來說,在 Redis 只運行單線程的情況下,該機制允許內(nèi)核中,同時存在多個監(jiān)聽套接字和已連接套接字。內(nèi)核會一直監(jiān)聽這些套接字上的連接請求或數(shù)據(jù)請求。一旦有請求到達,就會交給 Redis 線程處理,這就實現(xiàn)了一個 Redis 線程處理多個 IO 流的效果。

為了在請求到達時能通知到 Redis 線程,select/epoll 提供了基于事件的回調(diào)機制,即針對不同事件的發(fā)生,調(diào)用相應的處理函數(shù)。
那么,回調(diào)機制是怎么工作的呢?其實,select/epoll 一旦監(jiān)測到 FD 上有請求到達時,就會觸發(fā)相應的事件。
這些事件會被放進一個事件隊列,Redis 單線程對該事件隊列不斷進行處理。這樣一來,Redis 無需一直輪詢是否有請求實際發(fā)生,這就可以避免造成 CPU 資源浪費。同時,Redis 在對事件隊列中的事件進行處理時,會調(diào)用相應的處理函數(shù),這就實現(xiàn)了基于事件的回調(diào)。因為 Redis 一直在對事件隊列進行處理,所以能及時響應客戶端請求,提升 Redis 的響應性能。
redis中的I/O多路復用的所有功能通過包裝常見的select、epoll、evport和kqueue這些I/O多路復用函數(shù)庫來實現(xiàn)的。
因為 Redis 需要在多個平臺上運行,同時為了最大化執(zhí)行的效率與性能,所以會根據(jù)編譯平臺的不同選擇不同的 I/O 多路復用函數(shù)作為子模塊,提供給上層統(tǒng)一的接口;在 Redis 中,我們通過宏定義的使用,合理的選擇不同的子模塊
Redis 會優(yōu)先選擇時間復雜度為 ??(1)的 I/O 多路復用函數(shù)作為底層實現(xiàn),包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的這些函數(shù)都使用了內(nèi)核內(nèi)部的結(jié)構(gòu),并且能夠服務幾十萬的文件描述符。
但是如果當前編譯環(huán)境沒有上述函數(shù),就會選擇 select 作為備選方案,由于其在使用時會掃描全部監(jiān)聽的描述符,所以其時間復雜度較差 ??(??)
,并且只能同時服務 1024 個文件描述符,所以一般并不會以 select 作為第一方案使用。
Redis事件機制
redis 客戶端與 redis 服務端建立連接,發(fā)送命令,redis 服務器響應命令都是需要通過事件機制來做的,如下圖

多路復用 IO 模型的做法是,用一個線程將這一萬個建立成功的鏈接陸續(xù)的放入 event_poll,event_poll 會為這一萬個長連接注冊回調(diào)函數(shù),當某一個長連接準備就緒后(建立連接成功、數(shù)據(jù)讀取完成等),就會通過回調(diào)函數(shù)寫入到 event_poll 的就緒隊列 rdlist 中,這樣這個單線程就可以通過讀取 rdlist 獲取到需要的數(shù)據(jù)。
4. 小結(jié)
Redis真的只有單線程嗎?
Redis 單線程是指它對網(wǎng)絡 IO 和數(shù)據(jù)讀寫的操作采用了一個線程。
為什么使用單線程?
采用單線程的一個核心原因是避免多線程開發(fā)的并發(fā)控制問題。
單線程為什么這么快?
單線程的 Redis 也能獲得高性能,跟多路復用的 IO 模型密切相關(guān),因為這避免了 accept() 和 send()/recv() 潛在的網(wǎng)絡 IO 操作阻塞點。