乾貨!鞭辟入裏的Redis的實現原理,你知道是什麼嗎?

前言

最近咱們在更新Redis相關教程,之前咱們介紹了Redis的簡單架構和一些實戰應用,想來大家對Redis已經有了初步的認識。今天,咱們再來介紹一下Redis實現原理,希望幫助大家更加深入地瞭解Redis。

乾貨!鞭辟入裏的Redis的實現原理,你知道是什麼嗎?

 

原理1:鞭辟入裏——線程IO模型

Redis是個單線程程序!這點必須銘記。

也許你會懷疑高併發的Redis 中間件怎麼可能是單線程。很抱歉,它就是單線程,你的懷疑暴露了你基礎知識的不足。莫要瞧不起單線程,除了Redis 之外,Node.js 也是單線程,Nginx也是單線程,但是它們都是服務器高性能的典範。

Redis單線程爲什麼還能這麼快?

因爲它所有的數據都在內存中,所有的運算都是內存級別的運算。正因爲Redis 是單線程,所以要小心使用Redis 指令,對於那些時間複雜度爲0(n) 級別的指令,- -定要謹慎使用,一不小心就可能會導致Redis 卡頓。

Redis單線程如何處理那麼多的併發客戶端連接?

這個問題,有很多中高級程序員都無法回答,因爲他們沒聽過多路複用這個詞彙,不知道select 系列的事件輪詢API,沒用過非阻塞IO。

非阻塞IO

當我們調用套接字的讀寫方法,默認它們是阻塞的,比如 read 方法要傳遞進去一個參數 n,表示讀取這麼多字節後再返回,如果沒有讀夠線程就會卡在那裏,直到新的數據到來或者 連接關閉了,read 方法纔可以返回,線程才能繼續處理。而 write方法一般來說不會阻塞,除 非內核爲套接字分配的寫緩衝區已經滿了,write 方法就會阻塞,直到緩存區中有空閒空間挪出來了。

乾貨!鞭辟入裏的Redis的實現原理,你知道是什麼嗎?

 

非阻塞 IO 在套接字對象上提供了一個選項Non_Blocking,當這個選項打開時,讀寫方法不會阻塞,而是能讀多少讀多少,能寫多少寫多少。能讀多少取決於內核爲套接字分配的 讀緩衝區內部的數據字節數,能寫多少取決於內核爲套接字分配的寫緩衝區的空閒空間字節 數。讀方法和寫方法都會通過返回值來告知程序實際讀寫了多少字節。 有了非阻塞 IO 意味着線程在讀寫 IO 時可以不必再阻塞了,讀寫可以瞬間完成然後線程可以繼續幹別的事了。

事件輪詢 (多路複用)

非阻塞 IO 有個問題,那就是線程要讀數據,結果讀了一部分就返回了,線程如何知道 何時才應該繼續讀。也就是當數據到來時,線程如何得到通知。寫也是一樣,如果緩衝區滿了,寫不完,剩下的數據何時才應該繼續寫,線程也應該得到通知。

乾貨!鞭辟入裏的Redis的實現原理,你知道是什麼嗎?

 

事件輪詢 API 就是用來解決這個問題的,最簡單的事件輪詢 API 是 select 函數,它是 操作系統提供給用戶程序的 API。輸入是讀寫描述符列表 read_fds & write_fds,輸出是與之對應的可讀可寫事件。同時還提供了一個 timeout參數,如果沒有任何事件到來,那麼就最多 等待 timeout 時間,線程處於阻塞狀態。一旦期間有任何事件到來,就可以立即返回。時間過 了之後還是沒有任何事件到來,也會立即返回。拿到事件後,線程就可以繼續挨個處理相應 的事件。處理完了繼續過來輪詢。於是線程就進入了一個死循環,我們把這個死循環稱爲事 件循環,一個循環爲一個週期。

每個客戶端套接字 socket都有對應的讀寫文件描述符。

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 系統調用同時處理多個通道描述符的讀寫事件,因此我們將這類系 統調用稱爲多路複用 API。現代操作系統的多路複用 API 已經不再使用 select系統調用,而 改用 epoll(linux)和 kqueue(freebsd & macosx),因爲 select 系統調用的性能在描述符特別多時性能會非常差。它們使用起來可能在形式上略有差異,但是本質上都是差不多的,都可以使 用上面的僞代碼邏輯進行理解。 服務器套接字 serversocket 對象的讀操作是指調用 accept 接受客戶端新連接。何時有新連接到來,也是通過 select 系統調用的讀事件來得到通知的。 事件輪詢 API 就是 Java 語言裏面的 NIO 技術 Java 的 NIO 並不是 Java 特有的技術,其它計算機語言都有這個技術,只不過換了一 個詞彙,不叫 NIO 而已。

指令隊列

Redis 會將每個客戶端套接字都關聯一個指令隊列。客戶端的指令通過隊列來排隊進行 順序處理,先到先服務。

響應隊列

Redis 同樣也會爲每個客戶端套接字關聯一個響應隊列。Redis 服務器通過響應隊列來將 指令的返回結果回覆給客戶端。 如果隊列爲空,那麼意味着連接暫時處於空閒狀態,不需要去獲取寫事件,也就是可以將當前的客戶端描述符從write_fds 裏面移出來。等到隊列有數據 了,再將描述符放進去。避免 select 系統調用立即返回寫事件,結果發現沒什麼數據可以 寫。出這種情況的線程會飆高 CPU。

定時任務

服務器處理要響應 IO 事件外,還要處理其它事情。比如定時任務就是非常重要的一件事。如果線程阻塞在 select 系統調用上,定時任務將無法得到準時調度。那 Redis 是如何解決這個問題的呢?

Redis 的定時任務會記錄在一個稱爲最小堆的數據結構中。這個堆中,最快要執行的任務排在堆的最上方。在每個循環週期,Redis 都會將最小堆裏面已經到點的任務立即進行處 理。處理完畢後,將最快要執行的任務還需要的時間記錄下來,這個時間就是select 系統調 用的 timeout 參數。因爲 Redis 知道未來 timeout 時間內,沒有其它定時任務需要處理,所以可以安心睡眠 timeout 的時間。

後記

Redis實現原理其實有很多的,咱們今天主要介紹一個,爲的就是讓大家深入理解,每天進步一點點,量變產生質變,願大家都有一個美好的前途~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章