天天看點

Redis之線程IO模型非阻塞IO多路複用(事件輪詢)指令隊列響應隊列定時任務

Redis是一個單線程的應用程式,NodeJs、Nginx都是單線程,它們都屬于伺服器高性能的典範。

Redis之是以是單線程還能這麼快的原因,

其一是因為它所有的資料都在記憶體當中,所有的運算都是記憶體級别的運算,是以使用redis時,要注意時間複雜度為O(n)的指令,因為是單線程的,如果資料量太大,會讓其他指令被阻塞等待,

其二是因為redis使用非阻塞IO與多路複用處理大量的用戶端連接配接。

非阻塞IO

當我們使用套接字的讀寫方法時,預設是阻塞的,

即調用read方法傳遞一個參數n,表示最多讀取n個位元組後傳回,如果一個位元組都沒有,線程就會在read方法這裡持續等待,直到有資料過來或者連接配接被關閉,read方法此時傳回,線程才能執行下面的邏輯,

write方法一般不會阻塞,除非核心為套接字配置設定的寫緩沖區滿了,write方法才會阻塞,一直到緩存區中有空間閑出來。

下圖是套接字讀寫的細節流程。

Redis之線程IO模型非阻塞IO多路複用(事件輪詢)指令隊列響應隊列定時任務

非阻塞IO在使用套接字時提供了一個選項Non_Blocking,當這個選項打開時,讀寫方法不會阻塞,而是能讀多少讀多少,能寫多少寫多少,

能讀多少取決與核心為套接字配置設定的讀緩沖區的資料位元組數,能寫多少取決于核心為套接字寫緩沖區配置設定的資料位元組數,

讀寫方法都會通過傳回值告訴程式讀寫了多少位元組數。

非阻塞IO意味着讀寫時,線程不必再被阻塞着,讀寫可以瞬間完成,線程可以繼續往下做别的事情。

多路複用(事件輪詢)

非阻塞IO雖然很快,但是也帶來一個問題,線程讀資料,讀了一部分就傳回了,沒有讀完,剩下的資料何時繼續讀?,寫資料,緩沖區滿了,沒有寫完,剩下的資料何時繼續寫?

當可以繼續讀或者可以繼續寫時,應該給應用程式一個通知,告訴應用程式可以繼續讀或者繼續寫,事件輪詢API就是用來處理這個問題的。

select

作業系統提供了一個select函數給使用者程式,輸入是讀寫描述符清單 read_fds & write_fds,輸出是與之對應的可讀可寫事件,

同時還提供了timeout參數,線程最多等待timeout的時間,在這期間有事件過來,方法立刻傳回,線程往下處理,如果超過timeout時間,方法也會傳回,

如果拿到事件了,線程即可挨個處理相應的事件,處理完了以後繼續調用 select api 輪詢,是以該線程其實是一個死循環,不停的 select,不停的處理,來回這樣,這個死循環被稱之為事件循環,一個循環即一個周期。

Redis之線程IO模型非阻塞IO多路複用(事件輪詢)指令隊列響應隊列定時任務

事件循環僞代碼:

while True
    read_events, write_events = select(read_fds, write_fds, timeout)
    for event in read_events:
        handle_read(event.fd)
    for event in write_events:
        handle_write(event.fd)
    handle_others() # 做其他的邏輯處理,處理定時任務等等           

通過select函數我們可以處理多個通道描述符的讀寫事件,是以将select這類的系統函數調用稱之為多路複用API,

現代作業系統的多路複用API已經不使用select系統調用,改用epoll(linux)和kqueue(FreeBSD、macosx),

select的性能在描述符變多時會變得很差,epoll與select使用起來略有差異,不過都可以用上面的僞代碼了解,都是當描述符發生事件時,循環對描述符的事件做出處理,

serversocket對象的讀操作是指調用accept接受用戶端新連接配接,何時有連接配接來臨,也是通過select調用的讀事件通知的。

Java中的NIO技術就是事件輪詢,其他語言也有這個技術。

指令隊列

Redis為每一個用戶端套接字關聯一個指令隊列,用戶端發來的指令通過隊列進行先進先出的順序處理。

響應隊列

同樣Redis傳回的結果也通過為每個用戶端關聯的一個隊列傳回,如果隊列為空,則暫時不需要去擷取寫事件,

此時會将該用戶端描述符從write_fds裡移除,等隊列有資料的時候,再将描述符放進去,這樣可以避免select系統調用傳回寫事件時,發現沒資料可寫,造成空輪詢、無用輪詢,對機器CPU的消耗。

定時任務

伺服器不單要響應IO事件,有些其他的事情也需要處理,例如應用程式自身的定時任務,如果線程阻塞在select調用上,等待select的傳回,這會造成有些定時任務到期了,卻沒有執行,

Redis的定時任務記錄在一個稱為 最小堆 的資料結構中,這個堆中,最快要執行的任務排在最上方,每個循環周期裡,redis會對堆中已經到時間點的任務進行處理,

處理完畢後,将堆中即将要執行的任務還需要的時間記錄下來,再次調用select時,這個時間就是timeout的值,在這期間内不會有其他任務需要執行了,redis可以放心的最多阻塞這麼久,然後到時間後進行相應的處理。

NodeJs和Nginx的事件處理原理和Redis也是類似的形式。