Redis為什么是單線程,高並發快的3大原因詳解


一、Redis的高並發和快速原因

1.redis是基於內存的,內存的讀寫速度非常快;

2.redis是單線程的,省去了很多上下文切換線程的時間;

3.redis使用多路復用技術,可以處理並發的連接。非阻塞IO 內部實現采用epoll,采用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連接都轉化成了事件,然后利用epoll的多路復用特性,絕不在io上浪費一點時間。

下面重點介紹單線程設計和IO多路復用核心設計快的原因。

1、為什么Redis是單線程的

1.官方答案

因為Redis是基於內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成為瓶頸,那就順理成章地采用單線程的方案了。

2.性能指標

關於redis的性能,官方網站也有,普通筆記本輕松處理每秒幾十萬的請求。

3.詳細原因

1)不需要各種鎖的性能消耗

Redis的數據結構並不全是簡單的Key-Value,還有list,hash等復雜的結構,這些結構有可能會進行很細粒度的操作,比如在很長的列表后面添加一個元素,在hash當中添加或者刪除

一個對象。這些操作可能就需要加非常多的鎖,導致的結果是同步開銷大大增加。

總之,在單線程的情況下,就不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗。

2)單線程多進程集群方案

單線程的威力實際上非常強大,每核心效率也非常高,多線程自然是可以比單線程有更高的性能上限,但是在今天的計算環境中,即使是單機多線程的上限也往往不能滿足需要了,需要進一步摸索的是多服務器集群化的方案,這些方案中多線程的技術照樣是用不上的。

所以單線程、多進程的集群不失為一個時髦的解決方案。

3)CPU消耗

采用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU。

但是如果CPU成為Redis瓶頸,或者不想讓服務器其他CUP核閑置,那怎么辦?

可以考慮多起幾個Redis進程,Redis是key-value數據庫,不是關系數據庫,數據之間沒有約束。只要客戶端分清哪些key放在哪個Redis進程上就可以了。

2、Redis單線程的優劣勢

單線程模型
  Redis客戶端對服務端的每次調用都經歷了發送命令,執行命令,返回結果三個過程。其中執行命令階段,由於Redis是單線程來處理命令的,所有每一條到達服務端的命令不會立刻執行,所有的命令都會進入一個隊列中,然后逐個被執行。並且多個客戶端發送的命令的執行順序是不確定的。但是可以確定的是不會有兩條命令被同時執行,不會產生並發問題,這就是Redis的單線程基本模型。

單進程單線程優勢

  1. 代碼更清晰,處理邏輯更簡單
  2. 不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的性能消耗
  3. 不存在多進程或者多線程導致的切換而消耗CPU

單進程單線程弊端

  1. 無法發揮多核CPU性能,不過可以通過在單機開多個Redis實例來完善;

2、IO多路復用技術

redis 采用網絡IO多路復用技術來保證在多連接的時候, 系統的高吞吐量。

 

多路-指的是多個socket連接,復用-指的是復用一個線程。多路復用主要有三種技術:select,poll,epoll。epoll是最新的也是目前最好的多路復用技術。

 

  這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成為這里的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。

 

二、Redis高並發快總結

1. Redis是純內存數據庫,一般都是簡單的存取操作,線程占用的時間很多,時間的花費主要集中在IO上,所以讀取速度快。

2. 再說一下IO,Redis使用的是非阻塞IO,IO多路復用,使用了單線程來輪詢描述符,將數據庫的開、關、讀、寫都轉換成了事件,減少了線程切換時上下文的切換和競爭。

3. Redis采用了單線程的模型,保證了每個操作的原子性,也減少了線程的上下文切換和競爭。

4. 另外,數據結構也幫了不少忙,Redis全程使用hash結構,讀取速度快,還有一些特殊的數據結構,對數據存儲進行了優化,如壓縮表,對短數據進行壓縮存儲,再如,跳表,使用有序的數據結構加快讀取的速度。

5. 還有一點,Redis采用自己實現的事件分離器,效率比較高,內部采用非阻塞的執行方式,吞吐能力比較大。

以上就是redis高並發快的詳解。


三、IO多路復用探討

1、基礎知識

操作系統

 
操作系統本身可以看做一個特殊的軟件,只有操作系統能直接接觸計算機硬件,其他軟件要訪問硬件都必須通過操作系統
操作系統對硬件做抽象,封裝接口給軟件調用
 
程序員
軟件
操作系統(在內存中)
IO設備(網卡、磁盤、鍵盤、鼠標等)

 

常見的操作系統有:
Windows、Linux、Mac

內核和用戶空間

首先,內核和用戶空間都在內存中

內核:
只要計算機處於開啟狀態,操作系統(記住它是一種特殊的軟件)就會把它的程序和數據放入內存中,由於操作系統的重要性,它會獨占內存中的一塊區域,這塊區域就稱之為內核
即,內核就是操作系統常駐內存的區域
用戶空間:
而用戶空間,就是某個進程啟動后,被分配到的一塊內存區域
注:
具體的涉及到虛擬地址空間的概念,想要詳細了解的童鞋可以看下《深入理解計算機系統》第三版的第十章“虛擬存儲器”

IO 

 
具體到 IO,操作系統提供了一些 IO 操作,用於操作 IO 設備的數據,然后應用軟件對其進行封裝
數據流轉:
網絡IO:網卡 → 內核 → 用戶空間(用戶內存)
磁盤IO:磁盤  → 內核 → 用戶空間(用戶內存) 
 
 
各個操作系統分別提供了哪些 IO 操作?有哪幾種 IO 模型?
IO 操作:
select、poll、epoll、kqueue、evport
IO 模型:
以 Linux 為例,它有五種 IO 模型:
阻塞IO模型、非阻塞IO模型、IO復用模型、信號驅動IO模型以及異步IO模型
可參考此鏈接中的內容,講的很細:

2、舉例說明

假設你是一個機場的空管, 你需要管理到你機場的所有的航線, 包括進港,出港, 有些航班需要放到停機坪等待,有些航班需要去登機口接乘客。

你會怎么做?

最簡單的做法,就是你去招一大批空管員,然后每人盯一架飛機, 從進港,接客,排位,出港,航線監控,直至交接給下一個空港,全程監控。

那么問題就來了:
  • 很快你就發現空管塔里面聚集起來一大票的空管員,交通稍微繁忙一點,新的空管員就已經擠不進來了。
  • 空管員之間需要協調,屋子里面就1, 2個人的時候還好,幾十號人以后 ,基本上就成菜市場了。
  • 空管員經常需要更新一些公用的東西,比如起飛顯示屏,比如下一個小時后的出港排期,最后你會很驚奇的發現,每個人的時間最后都花在了搶這些資源上。
現實上我們的空管同時管幾十架飛機稀松平常的事情, 他們怎么做的呢?
他們用這個東西

 

這個東西叫flight progress strip. 每一個塊代表一個航班,不同的槽代表不同的狀態,然后一個空管員可以管理一組這樣的塊(一組航班),而他的工作,就是在航班信息有新的更新的時候,把對應的塊放到不同的槽子里面。

 

這個東西現在還沒有淘汰哦,只是變成電子的了而已。。

是不是覺得一下子效率高了很多,一個空管塔里可以調度的航線可以是前一種方法的幾倍到幾十倍。

如果你把每一個航線當成一個Sock(I/O 流), 空管當成你的服務端Sock管理代碼的話.

第一種方法就是最傳統的多進程並發模型 (每進來一個新的I/O流會分配一個新的進程管理。)
第二種方法就是I/O多路復用 (單個線程,通過記錄跟蹤每個I/O流(sock)的狀態,來同時管理多個I/O流 。)

其實“I/O多路復用”這個坑爹翻譯可能是這個概念在中文里面如此難理解的原因。所謂的I/O多路復用在英文中其實叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都會出這個圖:

 

於是大部分人都直接聯想到"一根網線,多個sock復用" 這個概念,包括上面的幾個回答, 其實不管你用多進程還是I/O多路復用, 網線都只有一根好伐。多個Sock復用一根網線這個功能是在內核+驅動層實現的

重要的事情再說一遍: I/O multiplexing 這里面的 multiplexing 指的其實是在單個線程通過記錄跟蹤每一個Sock(I/O流)的狀態(對應空管塔里面的Fight progress strip槽)來同時管理多個I/O流. 發明它的原因,是盡量多的提高服務器的吞吐能力。

 

是不是聽起來好拗口,看個圖就懂了.

 
在同一個線程里面, 通過撥開關的方式,來同時傳輸多個I/O流, (學過EE的人現在可以站出來義正嚴辭說這個叫“時分復用”了)。

 

什么,你還沒有搞懂“一個請求到來了,nginx使用epoll接收請求的過程是怎樣的”, 多看看這個圖就了解了。提醒下,ngnix會有很多鏈接進來, epoll會把他們都監視起來,然后像撥開關一樣,誰有數據就撥向誰,然后調用相應的代碼處理。

------------------------------------------
了解這個基本的概念以后,其他的就很好解釋了。

select, poll, epoll 都是I/O多路復用的具體的實現,之所以有這三個鬼存在,其實是他們出現是有先后順序的。

I/O多路復用這個概念被提出來以后, select是第一個實現 (1983 左右在BSD里面實現的)。

select 被實現以后,很快就暴露出了很多問題。
  • select 會修改傳入的參數數組,這個對於一個需要調用很多次的函數,是非常不友好的。
  • select 如果任何一個sock(I/O stream)出現了數據,select 僅僅會返回,但是並不會告訴你是那個sock上有數據,於是你只能自己一個一個的找,10幾個sock可能還好,要是幾萬的sock每次都找一遍,這個無謂的開銷就頗有海天盛筵的豪氣了。
  • select 只能監視1024個鏈接, 這個跟草榴沒啥關系哦,linux 定義在頭文件中的,參見FD_SETSIZE。
  • select 不是線程安全的,如果你把一個sock加入到select, 然后突然另外一個線程發現,尼瑪,這個sock不用,要收回。對不起,這個select 不支持的,如果你喪心病狂的竟然關掉這個sock, select的標准行為是。。呃。。不可預測的, 這個可是寫在文檔中的哦.

“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸氣

於是14年以后(1997年)一幫人又實現了poll, poll 修復了select的很多問題,比如
  • poll 去掉了1024個鏈接的限制,於是要多少鏈接呢, 主人你開心就好。
  • poll 從設計上來說,不再修改傳入數組,不過這個要看你的平台了,所以行走江湖,還是小心為妙。

其實拖14年那么久也不是效率問題, 而是那個時代的硬件實在太弱,一台服務器處理1千多個鏈接簡直就是神一樣的存在了,select很長段時間已經滿足需求。

但是poll仍然不是線程安全的, 這就意味着,不管服務器有多強悍,你也只能在一個線程里面處理一組I/O流。你當然可以那多進程來配合了,不過然后你就有了多進程的各種問題。

於是5年以后, 在2002, 大神 Davide Libenzi 實現了epoll.

epoll 可以說是I/O 多路復用最新的一個實現,epoll 修復了poll 和select絕大部分問題, 比如:
  • epoll 現在是線程安全的。
  • epoll 現在不僅告訴你sock組里面數據,還會告訴你具體哪個sock有數據,你不用自己去找了。

epoll 當年的patch,現在還在,下面鏈接可以看得到:
/dev/epoll Home Page

可是epoll 有個致命的缺點。。只有linux支持。比如BSD上面對應的實現是kqueue。

其實有些國內知名廠商把epoll從安卓里面裁掉這種腦殘的事情我會主動告訴你嘛。什么,你說沒人用安卓做服務器,尼瑪你是看不起p2p軟件了啦。

而ngnix 的設計原則里面, 它會使用目標平台上面最高效的I/O多路復用模型咯,所以才會有這個設置。一般情況下,如果可能的話,盡量都用epoll/kqueue吧。

詳細的在這里:
Connection processing methods

PS: 上面所有這些比較分析,都建立在大並發下面,如果你的並發數太少,用哪個,其實都沒有區別。 如果像是在歐朋數據中心里面的轉碼服務器那種動不動就是幾萬幾十萬的並發,不用epoll我可以直接去撞牆了
作者:levin
鏈接:https://www.zhihu.com/question/32163005/answer/255238636
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

IO 多路復用是5種I/O模型中的第3種,對各種模型講個故事,描述下區別:

故事情節為:老李去買火車票,三天后買到一張退票。參演人員(老李,黃牛,售票員,快遞員),往返車站耗費1小時。

1.阻塞I/O模型

老李去火車站買票,排隊三天買到一張退票。

耗費:在車站吃喝拉撒睡 3天,其他事一件沒干。

 

2.非阻塞I/O模型

老李去火車站買票,隔12小時去火車站問有沒有退票,三天后買到一張票。

耗費:往返車站6次,路上6小時,其他時間做了好多事。

 

3.I/O復用模型

1.select/poll

老李去火車站買票,委托黃牛,然后每隔6小時電話黃牛詢問,黃牛三天內買到票,然后老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,黃牛手續費100元,打電話17次

 

2.epoll

老李去火車站買票,委托黃牛,黃牛買到后即通知老李去領,然后老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,黃牛手續費100元,無需打電話

 

4.信號驅動I/O模型

老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李,然后老李去火車站交錢領票。

耗費:往返車站2次,路上2小時,免黃牛費100元,無需打電話

 

5.異步I/O模型

老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李並快遞送票上門。

耗費:往返車站1次,路上1小時,免黃牛費100元,無需打電話

 

1同2的區別是:自己輪詢

2同3的區別是:委托黃牛

3同4的區別是:電話代替黃牛

4同5的區別是:電話通知是自取還是送票上門

 

  一個epoll場景:一個酒吧服務員(一個線程),前面趴了一群醉漢,突然一個吼一聲“倒酒”(事件),你小跑過去給他倒一杯,然后隨他去吧,突然又一個要倒酒,你又過去倒上,就這樣一個服務員服務好多人,有時沒人喝酒,服務員處於空閑狀態,可以干點別的玩玩手機。至於epoll與select,poll的區別在於后兩者的場景中醉漢不說話,你要挨個問要不要酒,沒時間玩手機了。io多路復用大概就是指這幾個醉漢共用一個服務員。

epoll應用

    • redis
    • nginx

 

select/poll/epoll之間的區別

3、多路分離函數select

IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。

 

  如上圖所示,用戶線程發起請求的時候,首先會將socket添加到select中,這時阻塞等待select函數返回。當數據到達時,select被激活,select函數返回,此時用戶線程才正式發起read請求,讀取數據並繼續執行。

  從流程上來看,使用select函數進行I/O請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的I/O請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個I/O請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

Reactor(反應器模式)

 

 

  如上圖,I/O多路復用模型使用了Reactor設計模式實現了這一機制。通過Reactor的方式,可以將用戶線程輪詢I/O操作狀態的工作統一交給handle_events事件循環進行處理用戶線程注冊事件處理器之后可以繼續執行做其他的工作(異步),而Reactor線程負責調用內核的select函數檢查socket狀態。當有socket被激活時,則通知相應的用戶線程(或執行用戶線程的回調函數),執行handle_event進行數據讀取、處理的工作。由於select函數是阻塞的,因此多路I/O復用模型也被稱為異步阻塞I/O模型。注意,這里的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。一般在使用I/O多路復用模型時,socket都是設置為NONBLOCK的,不過這並不會產生影響,因為用戶發起I/O請求時,數據已經到達了,用戶線程一定不會被阻塞。

4、總結

  I/O 多路復用模型是利用select、poll、epoll可以同時監察多個流的 I/O 事件的能力,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll是只輪詢那些真正發出了事件的流),依次順序的處理就緒的流,這種做法就避免了大量的無用操作。這里“多路”指的是多個網絡連接,“復用”指的是復用同一個線程。采用多路 I/O 復用技術可以讓單個線程高效的處理多個連接請求(盡量減少網絡IO的時間消耗),且Redis在內存中操作數據的速度非常快(內存內的操作不會成為這里的性能瓶頸),主要以上兩點造就了Redis具有很高的吞吐量。

 

 參考文章:

https://zhuanlan.zhihu.com/p/58038188

https://blog.csdn.net/diweikang/article/details/90346020

https://www.zhihu.com/question/32163005

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM