聊聊redis單線程為什么能做到高性能和io多路復用到底是個什么鬼


1:io多路復用epoll 
io多路復用簡單來說就是一個線程處理多個網絡請求。
我們知道epoll in 的事件觸發是可讀了,這個比較好理解,比如一個連接過來,或者一個數據發送過來了,那么in事件就觸發了,那么out事件是如何觸發的呢?緩沖區可寫(有空的區域),就可以觸發,epoll有兩種模式LT(水平觸發)和ET(邊緣觸發),LT模式下,主要緩沖區數據一次沒有處理完,那么下次epoll_wait返回時,還會返回這個句柄;而ET模式下,緩沖區數據處理一次就結束,下次是不會再通知了,只在第一次返回.所以在ET模式下,一般是通過while循環,一次性讀完全部數據.epoll默認使用的是LT。
socket的緩沖區已經滿了,此時無法繼續send。此時異步程序的正確處理流程是調用epoll_wait,當socket緩沖區中的數據被對方接收之后,緩沖區就會有空閑空間可以繼續往里面寫數據,此時epoll_wait就會返回這個socket的EPOLLOUT事件,獲得這個事件時,你就可以繼續往socket中寫出數據。
redis的epoll使用的是默認的LT模式,只要寫緩沖區可寫時,就會不斷的觸發可寫事件,為了避免一直觸發可寫事件,redis是在有數據可寫的時候注冊寫事件,寫完之后就取消寫事件的注冊
epoll內部數據結構為紅黑樹和鏈表,紅黑樹保存了所有socket和監聽的事件信息,鏈表保存的是就緒的socket信息,就是那些就緒socket已經幫你整理好了。
那么,這個准備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到准備就緒鏈表里了。
如此,一顆紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表里的數據即可。
 
2:讀寫事件的注冊與刪除
當一個新的連接建立后,redis會創建一個redisClient對象,然后為這個socket向epoll注冊一個讀事件,直到RedisClient對象銷毀時才刪除讀事件,當redis讀到一個完整的命令並解析完成后,就會為socket向epoll注冊寫事件,將回復信息發給client之后,就會從epoll刪除剛注冊的寫事件,下個命令來了之后又會重復這個增刪寫事件的動作。
所以每個socket向epoll注冊銷毀一次讀事件,多次注冊銷毀寫事件,這樣做的目的:在我沒什么可寫的情況下你就別叫我寫了,我知道什么時候可寫 
 
3:redis單線程是怎么做到高性能的呢?
以前我一直在想一個問題:如果一個redis命令很長,redis接收處理這個命令就要100毫秒,那么別的命令會不會延遲100毫秒呢?后續命令處理會不會像消息隊列一樣積壓呢?
答案:不會。
上面我們已經說了epoll的原理,它不是讓我們一次處理完一個命令后,再去處理另一個命令,epoll是幫我們一次接收多個命令的部分數據(如果命令很短則是完整的數據),每個socket都有一個緩沖區,寫滿了就不能寫了,需要讀出來后才能繼續往里面寫,redis為每個client分配了一個變長緩沖區,從socket中讀出后存在緩沖區中,當接收到一個完整的命令,就解析並執行這個命令,然后把緩沖區后面的數據往前移動,反復利用這塊內存,當這塊內存超過一定值后就會釋放,在需要的時候重新分配一塊內存
也就是說epoll的水平觸發模式將一個較長的命令請求分成了多次接收,一次能接收多個命令的請求,天生就只支持高並發的,加上redis會將耗時的命令會分多次處理,保證了我們的讀寫操作都很快。
綜述單線程高性能的原因:
  • 1:純內存操作本來就很快
  • 2:redis使用epoll支持io多路復用,天生支持高並發請求
  • 3:redis將耗時的操作分多次處理,保證每次處理的時間都很短,保證了讀寫性能,如果數據很長的話處理時間就會變長,所以redis不建議保存太長的數據
還有redis6.0實現了多線程的功能,性能至少翻倍,那你還要問題單線程為什么性能高嗎?而且還是在數據的接收解析和數據的發送使用多線程的情況下,性能就至少翻倍了。可能是為了保證代碼的簡潔性,作者不願意使用多線程,為了提升性能用了多線程,也是部分功能使用多線程,操作redis數據庫的邏輯還是單線程,如果數據是寫少讀多的情況下,采用多線程讀寫鎖性能會不會提升很多呢?
所以redis一開始采用單線程的原因:
  • 1:代碼簡潔又簡單 
  • 2:性能已經很好了
  • 3:性能不夠我再搞多線程嗎
 
4:redis單線程是怎么同時處理文件事件和時間事件
文件事件主要是網絡I/O的讀寫,請求的接收和回復。時間事件就是單次/多次執行的定時器,如主從復制、定時刪除過期數據、字典rehash等。
redis所有核心功能都是跑在主線程中的,像aof文件落盤操作是在子線程中執行的,那么在高並發情況下它是怎么做到高性能的呢?
由於這兩種事件在同一個線程中執行,就會出現互相影響的問題,如時間事件到了還在等待/執行文件事件,或者文件事件已經就緒卻在執行時間事件,這就是單線程的缺點,所以在實現上要將這些影響降到最低。那么redis是怎么實現的呢?
定時執行的時間事件保存在一個鏈表中,由於鏈表中任務沒有按照執行時間排序,所以每次需要掃描單鏈表,找到最近需要執行的任務,時間復雜度是O(N),redis敢這么實現就是因為這個鏈表很短,大部分定時任務都是在serverCron方法中被調用。從現在開始到最近需要執行的任務的開始時間,時長定位T,這段時間就是屬於文件事件的處理時間,以epoll為例,執行epoll_wait最多等待的時長為T,如果有就緒任務epoll會返回所有就緒的網絡任務,存在一個數組中,這時我們知道了所有就緒的socket和對應的事件(讀、寫、錯誤、掛斷),然后就可以接收數據,解析,執行對應的命令函數。
如果最近要執行的定時任務時間已經過了,那么epoll就不會阻塞,直接返回已經就緒的網絡事件,即不等待。
總之單線程,定時事件和網絡事件還是會互相影響的,正在處理定時事件網絡任務來了,正在處理網絡事件定時任務的時間到了。所以redis必須保證每個任務的處理時間不能太長。 


免責聲明!

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



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