最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Redis網絡I/O模型的使用及說明

 更新時間:2025年12月24日 08:39:30   作者:姓蔡小朋友  
文章詳細介紹了Redis的網絡模型,包括單線程和多線程模型,以及I/O多路復用、事件通知機制、信號驅動I/O和異步I/O等技術,同時簡要介紹了Redis的通信協議RESP

聲明:這里的I/O表示redis從網卡讀來自客戶端的數據(包括指令、數據)

一、阻塞I/O

阻塞I/O讀取數據過程:

  • 應用程序(服務器)接收到客戶端的三次握手,通過accept()建立連接,得到一個socket文件描述符,用于與客戶端通信。
  • 應用程序進程調用read()函數,觸發(fā)系統調用,進程從用戶態(tài)切換到內核態(tài),自動執(zhí)行內核中的系統調用處理函數 sys_read()。
  • 內核態(tài)的該進程檢查客戶端的數據是否已經在內核空間的緩存中(第一次進入內核態(tài)肯定不在),如果數據在緩存中,直接拷貝到用戶空間,然后切換回用戶態(tài)。
  • 如果未命中說明客戶端發(fā)送的數據尚未到達,進程進入阻塞隊列讓出CPU,持續(xù)等待客戶端數據。
  • 客戶端發(fā)起HTTP請求,并攜帶數據。
  • 客戶端發(fā)送的網絡數據包到達服務器網卡,網卡通過DMA將數據包寫入內核緩沖區(qū),然后觸發(fā)硬件中斷,搶占CPU,執(zhí)行中斷處理程序。
  • 中斷處理程序將數據進行網絡協議棧的處理(如IP、TCP處理),最終將數據放入對應socket的接收緩沖區(qū),CPU會返回到被中斷的地方繼續(xù)執(zhí)行,喚醒等待在該socket上的進程,進入就緒態(tài)。
  • 當該進程被調度器選中再次運行時,它從之前阻塞的地方繼續(xù)執(zhí)行,此時數據已經在內核緩存中,于是將數據從內核緩存拷貝到socket的用戶空間。
  • 系統調用返回,進程從內核態(tài)切換回用戶態(tài),并繼續(xù)執(zhí)行用戶態(tài)代碼。
  • 處理完成后,進程重新進入內核態(tài)阻塞。

對于單線程I/O模型,服務器只有一個線程用來監(jiān)聽所有客戶端,線程每次調用recvfrom系統調用只能監(jiān)聽一個客戶端的數據,如果該客戶端沒有數據會一直阻塞直到該客戶端的數據到達內核緩沖區(qū),無法處理其他客戶端早已寫入內核緩沖區(qū)的數據,性能差。

二、非阻塞I/O

非阻塞I/O相較于阻塞I/O優(yōu)勢在于不需要阻塞在某一個客戶端,而是可以輪詢所有客戶端的狀態(tài),當某一客戶端的數據可用會調用recvfrom讀數據到用戶空間,并處理該數據。

非阻塞I/O讀取過程:和阻塞I/O的主要區(qū)別在步驟4和8

  • 應用程序(服務器)接收到客戶端的三次握手,通過accept()建立連接,得到一個socket文件描述符,用于與客戶端通信。
  • 應用程序進程調用read()函數,觸發(fā)系統調用,進程從用戶態(tài)切換到內核態(tài),自動執(zhí)行內核中的系統調用處理函數 sys_read()。
  • 內核態(tài)的該進程檢查客戶端的數據是否已經在內核空間的緩存中,如果數據在緩存中,直接拷貝到用戶空間,然后切換回用戶態(tài)。
  • 如果未命中說明客戶端發(fā)送的數據尚未到達,進程返回用戶態(tài)并輪詢1~4步,此時進程不會阻塞,也不會讓出CPU。
  • 客戶端發(fā)起HTTP請求,并攜帶數據。
  • 客戶端發(fā)送的網絡數據包到達服務器網卡,網卡通過DMA將數據包寫入內核緩沖區(qū),然后觸發(fā)硬件中斷,搶占CPU,執(zhí)行中斷處理程序。
  • 中斷處理程序將數據進行網絡協議棧的處理(如IP、TCP處理),最終將數據放入對應socket的接收緩沖區(qū),CPU會返回到被中斷的地方繼續(xù)執(zhí)行,喚醒等待在該socket上的進程,進入就緒態(tài)。
  • 進程輪詢調用read()時,進程從用戶態(tài)切換到內核態(tài),此時數據已經在內核緩存中,于是將數據從內核緩存拷貝到socket的用戶空間
  • 系統調用返回,進程從內核態(tài)切換回用戶態(tài),并繼續(xù)執(zhí)行用戶態(tài)代碼。
  • 處理完成后,進程重新進入內核態(tài)阻塞。

非阻塞I/O雖然不需要阻塞等待某一客戶端的請求,并且可以同時while輪詢監(jiān)聽多個客戶端的請求,但是while輪詢會查詢所有客戶端的數據是否到達,線程一直占用CPU,導致CPU利用率低,且輪詢所有客戶端所以性能也較差。

三、I/O多路復用*

I/O多路復用是利用單個線程同時監(jiān)聽多個文件描述符(每個文件描述符對應一個socket,也就是客戶端),也就是說單線程可以 同時 監(jiān)聽多個socket的網絡I/O請求。與阻塞I/O不同的是,I/O多路復用使用的是select、poll、epoll系統調用函數,該函數在阻塞狀態(tài)下可以同時監(jiān)聽多個socket[監(jiān)聽socket、客戶端socket],當某一個socket的數據到達時,就會喚醒阻塞線程回到用戶態(tài),線程調用recvfrom來讀取已經到達的數據到用戶空間并處理。與非阻塞I/O不同的是,當沒有數據可到達時線程會阻塞并讓出CPU。

1.select系統調用

select為讀請求、寫請求、異常事件創(chuàng)建三個獨立的數組,每個數組有32個元素,每個元素占32bit,使用bit位作為標記,因此每個數組可監(jiān)聽1024個請求,其中請求來自不同的線程。通過timeout設定select阻塞等待的超時時間。

單線程下select I/O多路復用讀取網絡數據過程:

  • 單線程(服務器)創(chuàng)建監(jiān)聽socket,用來監(jiān)聽新客戶端的連接請求,將監(jiān)聽socket的fd記錄到read數組中的某個bit位上。
  • 單線程調用select()函數,觸發(fā)系統調用,進程從用戶態(tài)切換到內核態(tài),并將三個數組拷貝到內核態(tài)。
  • 內核態(tài)的該線程檢查緩存中是否有數據,如果有,判斷數據是哪個fd的,并判斷是讀請求、寫請求還是出現異常,將fd對應數組位置的bit置0,切換回用戶態(tài),并將修改后的數組拷貝到用戶態(tài)。
  • 如果未命中說明沒有客戶端發(fā)送數據,線程進入阻塞隊列讓出CPU,持續(xù)監(jiān)聽客戶端數據。
  • 如果阻塞時間超過了timeout,那么喚醒該線程,回到用戶態(tài)。
  • 客戶端A或B發(fā)起HTTP請求,并攜帶數據。
  • 客戶端A或B發(fā)送的網絡數據包到達服務器網卡,網卡通過DMA將數據包寫入內核緩沖區(qū),然后觸發(fā)硬件中斷,搶占CPU,執(zhí)行中斷處理程序。
  • 中斷處理程序將數據進行網絡協議棧的處理(如IP、TCP處理),最終將數據放入對應socket的接收緩沖區(qū),修改對應socket的三個數組,CPU會返回到被中斷的地方繼續(xù)執(zhí)行,喚醒單線程,進入就緒態(tài)。
  • 當該線程被調度器選中再次運行時,回到用戶態(tài),并將修改后的數組拷貝到用戶態(tài)。
  • 線程遍歷三個數組篩選出哪些客戶端的數據已經在內核空間的緩存中,記為t[fd1,fd3…]。
  • 如果t[]中有監(jiān)聽socket的fd,說明有新的客戶端請求連接,那么單線程調用accept()系統調用與客戶端建立連接,得到一個客戶端socket文件描述符fd,用于與該客戶端通信,將fd添加到三個數組的某個bit位上。
  • t[]中除了監(jiān)聽socket之外的其他fd說明是已建立連接的客戶端發(fā)送的數據,單線程對t[]調用read()函數,進程從用戶態(tài)切換到內核態(tài),此時t[]的數據一定在內核緩存中,直接拷貝到對應fd的用戶空間,然后切換回用戶態(tài)。
  • 系統調用返回,線程從內核態(tài)切換回用戶態(tài),并繼續(xù)執(zhí)行用戶態(tài)代碼。
  • 處理完成后,線程傳入添加新客戶端fd后的三個數組重新進入內核態(tài)阻塞監(jiān)聽。

select缺點:

  • fd_set由于可變,需要頻繁的修改用戶空間和內核空間中的fd_set
  • 在用戶態(tài)需要遍歷原fd_set和修改后的fd_set才能知道那些數據就緒
  • 監(jiān)聽數量不超過1024

2.poll系統調用

poll仍然使用數組的方式監(jiān)聽不同的請求,但數組中每個元素都是一個結構體,記錄了請求的文件描述符fd(socket、客戶端)、請求類型events、返回值revents。單線程在內核中監(jiān)聽時(等待中斷響應),在超時時間內若監(jiān)聽到某個文件描述符的中斷響應,就將響應類型記錄到revents中,否則revents置0表示未收到相應。

單線程下poll I/O多路復用讀取網絡數據過程:

  • 監(jiān)聽socket接收到客戶端A的三次握手,通過accept()建立連接,得到一個socket文件描述符,將文件描述符、要監(jiān)聽的事件類型記錄到polled數組中。
  • 單線程調用poll()函數,觸發(fā)系統調用,進程從用戶態(tài)切換到內核態(tài),并將polled數組拷貝到內核態(tài)
  • 內核態(tài)的該線程檢查緩存中是否有數據,如果有,判斷數據是哪個fd的,并判斷是讀請求、寫請求還是出現異常,將fd對應結構體的revents置為相應值,切換回用戶態(tài),并將修改后的數組拷貝到用戶態(tài),返回就緒文件描述符數量n
  • 線程判斷n是否大于0,大于0則遍歷polled數組,找到就緒的文件描述符。
  • …同select

3.epoll系統調用

epoll維護一棵紅黑樹和一個鏈表,紅黑樹記錄要監(jiān)聽的文件描述符fd,就緒鏈表記錄已就緒的文件描述符fd。與select最大的不同在于,epoll在內核態(tài)初始化epoll_create系統調用),不會頻繁的在用戶態(tài)和內核態(tài)拷貝。

由于在內核態(tài),所以當線程通過accept()與客戶端建立連接后,需要通過epoll_ctl()系統調用向epoll的紅黑樹中添加fd。此外,添加時會對fd設置一個回調函數,回調函數會在fd的數據寫入內核緩存(觸發(fā)中斷響應)時將fd添加到就緒鏈表中。

線程使用epoll_wait()系統調用持續(xù)監(jiān)聽就緒鏈表,若在超時時間內有fd加入到就緒鏈表中那么喚醒該線程,將就緒的fd拷貝到用戶空間。

epoll方案很好的解決了select的三個問題。

單線程下epoll I/O多路復用讀取網絡數據過程:

  • 單線程調用epoll_create系統調用進入內核態(tài),在內核態(tài)初始化一棵紅黑樹和一個鏈表,回到用戶態(tài)。
  • 單線程(服務器)創(chuàng)建監(jiān)聽socket,用來監(jiān)聽新客戶端的連接請求。
  • 單線程調用epoll_ctl()系統調用進入內核態(tài),向紅黑樹中添加監(jiān)聽socket的fd,回到用戶態(tài)。
  • 單線程調用epoll_wait()函數,觸發(fā)系統調用,進程從用戶態(tài)切換到內核態(tài)
  • 內核態(tài)的該線程檢查就緒鏈表中是否有fd,如果有,切換回用戶態(tài),并將就緒鏈表拷貝到用戶態(tài)。
  • 如果未命中說明沒有客戶端發(fā)送數據,線程進入阻塞隊列讓出CPU,持續(xù)等待客戶端數據。
  • 如果阻塞時間超過了timeout,那么喚醒該線程,回到用戶態(tài)。
  • 客戶端A或B發(fā)起HTTP請求,并攜帶數據。
  • 客戶端A或B發(fā)送的網絡數據包到達服務器網卡,網卡通過DMA將數據包寫入內核緩沖區(qū),然后觸發(fā)硬件中斷,搶占CPU,執(zhí)行中斷處理程序。
  • 中斷處理程序將數據進行網絡協議棧的處理(如IP、TCP處理),最終將數據放入對應socket的接收緩沖區(qū),CPU會返回到被中斷的地方繼續(xù)執(zhí)行,觸發(fā)回調函數將數據對應的socket(fd)加入就緒鏈表,喚醒等待在該socket上的線程,進入就緒態(tài)。
  • 當該線程被調度器選中再次運行時,切換回用戶態(tài),并將就緒鏈表拷貝到用戶態(tài)。
  • 如果就緒鏈表中有監(jiān)聽socket的fd,說明有新的客戶端請求連接,那么單線程調用accept()系統調用與客戶端建立連接,得到一個客戶端socket文件描述符fd,用于與該客戶端通信,調用epoll_ctl()系統調用向紅黑樹中添加新客戶端socket的fd,回到用戶態(tài)。
  • 就緒鏈表中除了監(jiān)聽socket之外的其他fd說明是已建立連接的客戶端發(fā)送的數據,單線程對就緒鏈表中的fd調用read()函數,進程從用戶態(tài)切換到內核態(tài),此時就緒鏈表中fd的數據一定在內核緩存中,直接拷貝到對應fd的用戶空間,然后切換回用戶態(tài)。
  • 系統調用返回,線程從內核態(tài)切換回用戶態(tài),并繼續(xù)執(zhí)行用戶態(tài)代碼。
  • 線程解析并處理命令,然后將響應結果寫入客戶端socket(fd)的發(fā)送緩沖區(qū),調用write或send系統調用將響應結果通過DMA從內核的發(fā)送緩沖區(qū)由網卡發(fā)送出去。
  • 處理完成后,重新進入內核態(tài)阻塞。

思考:有沒有可能連接請求還沒有處理完,沒有為客戶端socket分配內核緩沖區(qū),客戶端的讀請求就來了?

應該不會,這個連接請求應該就是TCP三次握手,只有建立連接了,服務器向客戶端發(fā)送ACK,客戶端才能發(fā)讀請求到服務器。

事件通知機制:

LevelTriggered:LT,每次將就緒隊列中的fd拷貝到用戶空間時保留就緒隊列中的fd。

  • 可以實現重復讀取,適用于fd一次讀不全的情況。

EdgeTriggered:ET,每次將就緒隊列中的fd拷貝到用戶空間時清空就緒隊列。

  • 適用于一次性讀取fd的情況。
  • 可以通過epoll_ctl函數的修改功能手動將fd添加回就緒鏈表實現LT的效果

不太理解為什么用LT,每次都從網絡讀到用戶內存就算一次讀不全早晚也能讀完吧,沒必要每次都重復讀吧,可能是因為數據在對應用戶空間中不連續(xù)使用起來麻煩。

四、信號驅動I/O

信號驅動I/O通過sigaction()系統調用對fd設置回調函數,此時線程立即返回用戶態(tài)執(zhí)行其他任務。當fd的數據到達內核緩存時會觸發(fā)回調函數,將就緒的fd拷貝到用戶態(tài)的信號隊列通知線程。線程調用recvfrom進入內核態(tài)將數據拷貝到用戶內存。

與非阻塞I/O不同的是,線程返回用戶態(tài)后不會輪詢數據是否準備好,而是去執(zhí)行其他任務,收到來自內核的通知才調用read讀數據。

由于信號驅動I/O下每有一個fd就緒內核線程都會執(zhí)行內核態(tài)與用戶態(tài)切換通知用戶線程,影響性能;而多路復用I/O可以一次性獲取多個就緒的fd才切換到用戶態(tài),性能更好(多路復用I/O中雖然每次fd到達就緒鏈表都會喚醒線程,但是線程獲得CPU前仍然可以有fd進入就緒鏈表,且調用wait()進入內核態(tài)時如果就緒鏈表有多個fd也可以一次性返回)。因此多路復用I/O的性能更好更適用于有高并發(fā)需求的redis。

五、異步I/O

異步I/O整個過程都是非阻塞的,用戶進程調用aio_read()系統調用函數聲明要讀的fd和讀到用戶內存空間的地址就直接返回到用戶態(tài),進行其他任務。而讀取數據和將數據從內核緩存拷貝到用戶內存的任務完全交由內核線程來完成。

異步I/O比I/O多路復用更高效,因為用戶線程對I/O操作完全解耦,可以實現更高并發(fā)的處理請求,使用頻率較高但是不如多路復用I/O:由于所有任務都交由內核完成,每個任務內核都要開辟新線程來處理(內核是多線程,CPU也是多核,只有用戶應用redis是單線程),對內核負載太大。解決方法是在用戶應用進行并發(fā)控制,限制單位時間向內核分配的任務數量。

六、Redis網絡模型*

1.純單線程模型

Redis通過I/O多路復用來提高網絡性能,支持各種不同的多路復用實現,將這些實現封裝為統一的接口:

  • ae_epoll:LinuxOS多路復用實現方案
  • ae_kqueue:MacOS
  • ae_select:所有OS

Redis提供了通用的API接口,針對不同操作系統使用不同的實現方案。

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

  • AddEvent():注冊fd,例如epoll_ctl()
  • Create():創(chuàng)建多路復用監(jiān)聽器,例如epoll_create()
  • DelEvent():刪除fd,例如epoll_ctl()
  • Poll():監(jiān)聽fd就緒,例如epoll_wait()、select()、poll()

Linux下redis(單線程) epoll 處理網絡數據過程:(相較于三.3沒有內核態(tài)的切換,但是流程更完整)

  • 單線程調用epoll_create在內核態(tài)初始化一棵紅黑樹和一個鏈表。
  • 單線程創(chuàng)建監(jiān)聽socket,用來監(jiān)聽新客戶端的連接請求。
  • 單線程調用epoll_ctl向紅黑樹中添加監(jiān)聽socket的fd,設為可讀狀態(tài),并添加回調函數。
  • 為監(jiān)聽socket添加監(jiān)聽讀處理函數。
  • 單線程調用epoll_wait,阻塞并監(jiān)聽就緒鏈表,讓出CPU。
  • 如果阻塞時間超過了timeout,那么喚醒該線程。
  • 新客戶端A和B發(fā)起HTTP請求,請求建立連接
  • DMA和中斷處理程序將客戶端A的數據讀到監(jiān)聽socket的接收緩沖區(qū),觸發(fā)回調函數將監(jiān)聽socket的fd加入就緒鏈表,標記為讀請求。
  • 單線程監(jiān)聽到就緒鏈表中數據發(fā)生變化,進入就緒態(tài)。
  • 單線程就緒態(tài)到運行態(tài)過程中可能DMA和中斷處理程序將客戶端B的數據讀到監(jiān)聽socket的接收緩沖區(qū),觸發(fā)回調函數將監(jiān)聽socket的fd加入就緒鏈表。
  • 單線程獲取CPU,此時就緒鏈表中只有監(jiān)聽socket的fd,會觸發(fā)監(jiān)聽socket的監(jiān)聽讀處理函數:單線程調用accept()系統調用與客戶端A和B建立連接,分別得到客戶端A和B的socket文件描述符fd,為客戶端fd分別設為可讀狀態(tài)添加讀處理函數,調用epoll_ctl系統調用向紅黑樹中添加客戶端A和B的fd。
  • 單線程調用epoll_wait,檢查就緒鏈表中是否有fd,如果有,說明step11過程中有新的網絡數據到達了內核緩存,切換回用戶態(tài)處理。
  • 如果未命中說明沒有I/O請求,線程阻塞并監(jiān)聽就緒鏈表,讓出CPU。
  • 客戶端A發(fā)來HTTP請求,并攜帶數據。
  • DMA和中斷處理程序將客戶端A的數據讀到客戶端A socket的接收緩沖區(qū),觸發(fā)回調函數將客戶端A的fd加入就緒鏈表,并設為讀請求。
  • 單線程監(jiān)聽到就緒鏈表中數據發(fā)生變化,進入就緒態(tài)。
  • 單線程獲取CPU,此時就緒鏈表中有客戶端A的fd,且為讀請求,會觸發(fā)客戶端A socket的讀處理函數:單線程調用read系統調用將請求攜帶的數據從內核緩沖區(qū)讀到為客戶端A分配的內存輸入緩沖區(qū)中。
  • 單線程解析并執(zhí)行數據中的指令,然后將響應結果寫入redis的clients_pending_write鏈表,調用epoll_ctl將客戶端A的fd狀態(tài)修改為可讀+可寫類型,并添加寫處理函數。(只有內核中該客戶端的緩沖區(qū)空間不夠了才放入鏈表,不然會直接發(fā)送到內核緩沖區(qū),且不會添加寫處理函數網卡會自動發(fā)送數據,step20不滿足,直接異步執(zhí)行step22,我理解的是數據先正常發(fā)送,只有發(fā)送空間滿了發(fā)不出去才設為可寫類型表示還有未發(fā)出去的數據,然后只有發(fā)送完后觸發(fā)的中斷處理程序檢查到有可寫標識才會將該fd的寫請求加入就緒鏈表)
  • 單線程調用epoll_wait,檢查就緒鏈表中是否有fd…
  • 當客戶端A的內核發(fā)送緩沖區(qū)有空閑且可寫狀態(tài)為true就會發(fā)出硬件中斷將客戶端A的fd加入到就緒鏈表,并設為寫請求
  • 單線程獲取CPU,此時就緒鏈表中有客戶端A的fd,且為寫請求,會觸發(fā)客戶端A socket的寫處理函數:單線程調用send從redis的clients_pending_write鏈表中取數據,將數據寫入客戶端A的內核發(fā)送緩沖區(qū),單線程檢查redis的clients_pending_write鏈表是否還有客戶端A的數據,如果沒有那么調用epoll_ctl將客戶端A的fd狀態(tài)修改為可讀類型
  • 內核協議棧會將數據通過網卡發(fā)送出去,發(fā)送成功后觸發(fā)中斷將客戶端A的fd加入到就緒鏈表,并設為寫請求(這個過程是異步的,由內核和網卡負責,不需要redis單線程參與)。
  • 單線程調用epoll_wait,檢查就緒鏈表中是否有fd…

因此,Redis中的I/O多路復用可以理解為:復用epoll同時監(jiān)聽客戶端連接請求、客戶端讀請求、服務器向客戶端的寫請求,就緒的任何類型的請求都會放到就緒鏈表中,并每隔一段時間接收多條請求,針對不同類型的請求使用不同的分支(監(jiān)聽讀處理函數step11、讀處理函數step17+18、寫處理函數step21)處理請求。

復用就緒鏈表接收三類不同的請求請求:

  • 客戶端連接請求從初始化epoll開始,調用epoll_create在內核態(tài)創(chuàng)建紅黑樹和就緒鏈表,創(chuàng)建監(jiān)聽socket后調用epoll_ctl向紅黑樹中添加監(jiān)聽socket的fd,設為可讀狀態(tài),并添加回調函數。當連接請求由網卡到達內核緩沖區(qū)會觸發(fā)中斷,觸發(fā)回調函數將監(jiān)聽socket從紅黑樹添加到就緒鏈表,設為讀請求。
  • 客戶端讀請求在建立連接后會調用epoll_ctl將該客戶端的fd添加到紅黑樹,設為可讀狀態(tài),該客戶端的讀請求到達后會被寫入為該客戶端分配的內核緩沖區(qū),觸發(fā)中斷觸發(fā)回調函數將該客戶端socket添加到就緒鏈表,設為讀請求。
  • 服務器向客戶端的寫請求在線程接收客戶端讀請求并處理后,如果該客戶端的內核發(fā)送緩沖區(qū)有足夠的空閑那么將數據直接發(fā)送到內核緩沖區(qū),會異步發(fā)送數據給客戶端,否則就會將該客戶端socket加入到pending_write鏈表中,并調用epoll_ctl將紅黑樹中客戶端的fd并設為可讀+可寫類型,當內核緩沖區(qū)有空閑也就是發(fā)送數據后會引發(fā)中斷觸發(fā)回調函數將客戶端的fd添加到就緒鏈表并設為寫請求。

復用單線程處理三類不同的請求請求:

線程調用epoll_wait進入內核態(tài)檢查就緒鏈表是否有數據,如果有直接將就緒鏈表拷貝到用戶態(tài)并處理,如果沒有回讓出CPU并阻塞,直到就緒鏈表有數據或超過等待時長會進入就緒態(tài)等待分配CPU回到用戶態(tài)?;氐接脩魬B(tài)會遍歷就緒鏈表依次處理每個請求:

  • 客戶端連接請求會調用之前添加的監(jiān)聽處理函數建立連接,調用epoll_ctl將客戶端fd加入到紅黑樹,設為可讀,并添加回調函數和讀處理函數。
  • 客戶端讀請求會調用之前添加的讀處理函數,執(zhí)行數據中的命令例如"get key"并將結果發(fā)送到內核發(fā)送緩沖區(qū),否則就會將該客戶端socket加入到pending_write鏈表中,調用epoll_ctl將紅黑樹中客戶端的fd并設為可讀+可寫類型。
  • 服務器向客戶端的寫請求會調用send從pending_write鏈表中取該客戶端數據,將數據寫入客戶端的內核發(fā)送緩沖區(qū),發(fā)送后如果pending_write鏈表已經沒有當前客戶端的數據,那么調用epoll_ctl將客戶端的fd狀態(tài)修改為可讀類型。

理一下寫處理的過程:首先線程接收客戶端讀請求并處理,處理完成如果該客戶端的內核緩沖區(qū)有足夠的空閑那么將數據直接發(fā)送到內核緩沖區(qū),后臺會異步發(fā)送數據。只有該客戶端的內核緩沖區(qū)滿了才將數據放到clients_pending_write鏈表中,并設為可寫類型,當該客戶端的內核緩沖區(qū)中的數據發(fā)送完后觸發(fā)中斷,中斷處理程序檢查有足夠的空閑了且該fd目前是可寫狀態(tài),那么會將該fd的寫請求放入就緒鏈表

Linux下redis(單線程) epoll 處理網絡數據過程:(從用戶態(tài)調用的角度分析,不涉及內核態(tài),多路復用的思想更直觀)

//redis網絡I/O入口函數
main {
	server.el = aeCreateEventLoop();// 創(chuàng)建epoll
	listenToPort(server.port,server.ip);// redis創(chuàng)建監(jiān)聽socket,給定監(jiān)聽的ip和port(服務器的ip)
	createSocketAcceptHandler(acceptTcpHandler);// 將監(jiān)聽socket添加到epoll,并添加監(jiān)聽讀處理函數(監(jiān)聽讀處理函數就是step11) 
	
	// 死循環(huán),該線程一直網絡I/O
	while(true){
		// 為clients_pending_write鏈表中(有寫需求)的客戶端socket依次添加寫處理函數(寫處理函數就是step21)
		foreach(clients_pending_write){
			connSetWriteHandlerWithBarrier(sendReplyToClient);
		}
		
		/* epoll_wait等待中斷信號,返回就緒鏈表
		客戶端發(fā)送的連接請求引發(fā)的中斷會將監(jiān)聽fd添加到就緒鏈表,設為讀請求
		客戶端發(fā)送的讀請求引發(fā)的中斷會將客戶端fd添加到就緒鏈表,設為讀請求
		服務器向客戶端發(fā)送返回結果后引發(fā)的中斷會將客戶端fd添加到就緒鏈表,設為寫請求
		*/
		numevents = aeApiPoll();
		// 依次處理就緒鏈表中的socket
		for(j = 0; j < numevents; j++){
			if(j is 監(jiān)聽socket){
				fd = accept(newClient);// 接收新客戶端的連接請求,得到客戶端socket的fd
				connSetReadHandler(fd, readQueryFromClient);// 將客戶端socket添加到epoll,并添加讀處理函數(讀處理函數就是step17+18)
			}
			
			if(j is 客戶端讀socket){
				connRead(c);// 將數據從內核緩沖區(qū)讀到內存中該客戶端的輸入緩沖區(qū)
				processInputBuffer(c);// 解析數據中的命令為字符串數組[set, name, jack]
				cmd = lookupCommand(c->argv[0]);// redis中命令是以鍵值對方式存儲的,通過key=set就能找到對應的函數體
				proc(cmd,c);// 執(zhí)行cmd命令,傳入數據c
				addReply();// 將命令執(zhí)行結果寫入該客戶端的等待隊列
			}	

			if(j is 客戶端寫socket){
			    if (!clientHasPendingReplies(c)) {// 檢查是否有數據要寫
			        aeDeleteFileEvent(server.el, fd, AE_WRITABLE);// 沒有數據,立即取消寫事件監(jiān)聽
			        return;
			    }
			    int nwritten = write(c->fd, c->buf + c->sentlen, c->bufpos - c->sentlen);// 有數據,直接寫入socket
			}	
		}
	}
}

2.命令處理單線程+網絡I/O多線程模型

2.1 Redis單線程網絡模型的瓶頸

Redis命令執(zhí)行部分必須是單線程。

為什么redis要做成單線程:

  • 除了持久化操作外,redis是純內存操作,因此執(zhí)行速度非???/strong>,限制redis性能的是網絡I/O延遲而不是指令執(zhí)行速度,短板效應下多線程也不會帶來性能提升。
  • 多線程會導致上下文切換,會帶來額外開銷。
  • 單線程是為了保證命令的隔離性,而多線程會有線程安全問題,使用鎖機制可以解決線程安全問題但是也會帶來額外的開銷。

經過上面的分析,redis的執(zhí)行過程為:接收網絡請求(step12~17)->執(zhí)行命令(step18)->返回響應結果(step19~22)

  • 接收請求:redis單線程阻塞等待,并行的網絡請求由內核的中斷處理程序并行處理,性能很強,但是redis單線程返回用戶態(tài)后需要串行執(zhí)行read系統調用將數據從內核態(tài)讀到用戶態(tài),且需要頻繁切換用戶態(tài)和內核態(tài),這是主要瓶頸
  • 執(zhí)行命令:redis單線程串行執(zhí)行命令,因為命令必須串行才能保證原子操作,必須保持現狀。
  • 返回響應:由redis單線程串行調用send()函數發(fā)送響應,且需要頻繁切換用戶態(tài)和內核態(tài)(雖然內核態(tài)發(fā)送響應可以異步并行執(zhí)行,但串行send效率太低),這是主要瓶頸。

由于redis性能收到網絡I/O的限制,經過上述分析,網絡I/O的接受請求、返回響應部分可以設計成多線程,且這兩部分不涉及命令的執(zhí)行,所以不會出現并發(fā)問題。

2.2 Redis多線程網絡模型

對于接收請求,由redis單線程分發(fā)“read系統調用將數據從內核態(tài)讀到用戶態(tài)(step17)”這一操作給多個子線程執(zhí)行,大大提高了讀取速度,且不會出現線程安全問題(不涉及請求數據中的命令執(zhí)行)。

數據讀取并解析完成后,由redis單(主)線程來順序執(zhí)行命令,內存執(zhí)行速度快,切能保證原子操作。雖然接受請求使用多線程,但速度上仍然無法保證指令執(zhí)行的主線程有100%的利用率。

對于返回響應,由redis單線程分發(fā)“send系統調用將數據從用戶態(tài)讀到內核態(tài)(step21)”這一操作給多個子線程執(zhí)行,大大提高了讀取速度,且不會出現線程安全問題。

雖然如此,但網絡I/O依舊是短板。

七、RESP協議

Redis是CS架構。

  • 服務端:在Linux操作系統中安裝Redis并啟動,指定IP地址和端口號。
  • 客戶端:redis-cli命令行、jedis。

通信過程分為兩步:

  • 客戶端向服務端的ip+port發(fā)送一條請求,攜帶數據(命令)。
  • 服務端在端口上持續(xù)監(jiān)聽請求,使用網絡I/O模型接收到請求,解析并執(zhí)行命令,返回響應結果給客戶端。

Redis中采用RESP協議來規(guī)定客戶端和服務器發(fā)送請求的規(guī)范。

1.數據類型

RESP通過首字節(jié)的字符來區(qū)分不同數據類型,常用的數據類型包括5種:

  • 單行字符串:首字節(jié)是‘+’,后面跟單行字符串,以CRLF(“\r\n”)結尾,無法使用特殊字符,二進制不安全,一般用于服務端返回響應。
  • Errors:首字節(jié)是’-',后面跟異常信息,以CRLF(“\r\n”)結尾。
  • 數值:首字節(jié)是’:',后面跟數字格式的字符串,以CRLF(“\r\n”)結尾。
  • 多行字符串:首字節(jié)是’$',后根字符串占用字節(jié)數量,后根字符串,二進制安全,最大支持512MB。

例如:$3\r\nabc\r\n

如果大小為0,代表空字符串:“$0\r\n\r\n”

如果大小為-1,則代表不存在: “$-1\r\n”

  • 數組:首字節(jié)是’*',后根元素個數,后根元素,元素數據類型不限,可以嵌套數組。

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關文章

  • Redis中管道操作的項目實踐

    Redis中管道操作的項目實踐

    Redis管道操作通過將多個命令一次性發(fā)送到服務器,減少了網絡往返次數,本文就來介紹一下Redis的管道操作,具有一定的參考價值,感興趣的可以了解一下
    2025-02-02
  • Python Redis如何執(zhí)行Lua腳本

    Python Redis如何執(zhí)行Lua腳本

    這篇文章主要介紹了Python Redis如何執(zhí)行Lua腳本問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-02-02
  • Redis實現短信登錄的示例代碼

    Redis實現短信登錄的示例代碼

    本文主要介紹了Redis實現短信登錄的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07
  • Redis?命令詳解與實戰(zhàn)案例

    Redis?命令詳解與實戰(zhàn)案例

    本文詳細介紹了Redis的基礎知識、核心數據結構與命令、高級功能與命令、最佳實踐與性能優(yōu)化,以及實戰(zhàn)應用場景,通過實戰(zhàn)案例,展示了如何使用Redis構建高性能應用系統,感興趣的朋友跟隨小編一起看看吧
    2025-11-11
  • 硬核!15張圖解Redis為什么這么快(推薦)

    硬核!15張圖解Redis為什么這么快(推薦)

    作為一名服務端工程師,工作中你肯定和 Redis 打過交道。Redis為什么快,這點想必你也知道,至少為了面試也做過準備,今天通過本文給大家介紹下,感興趣的朋友一起看看吧
    2020-10-10
  • Redis跳躍表的基本原理和實現

    Redis跳躍表的基本原理和實現

    本文主要介紹了Redis跳躍表的基本原理和實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-06-06
  • Redis 常用命令之基礎、進階與場景化實戰(zhàn)案例

    Redis 常用命令之基礎、進階與場景化實戰(zhàn)案例

    Redis常用命令全解析,涵蓋基礎、進階和場景化實戰(zhàn),包括字符串、哈希、列表、集合、有序集合等數據類型,以及發(fā)布訂閱、分布式鎖、事務等高級功能,本文給大家介紹Redis常用命令之基礎、進階與場景化實戰(zhàn)案例,感興趣的朋友一起看看吧
    2026-01-01
  • redis的啟動方式操作詳解

    redis的啟動方式操作詳解

    文章介紹了Redis的三種啟動方式,直接啟動、通過配置文件啟動和使用啟動腳本,還詳細說明了如何將啟動腳本復制到/etc/init.d目錄并設置開機自啟動,解決了使用chkconfig命令時可能出現的錯誤,感興趣的朋友跟隨小編一起看看吧
    2025-12-12
  • Redis數據庫安全詳解

    Redis數據庫安全詳解

    這篇文章主要為大家介紹了Redis數據庫安全詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • 淺談Redis 緩存的三大問題及其解決方案

    淺談Redis 緩存的三大問題及其解決方案

    Redis 經常用于系統中的緩存,這樣可以解決目前 IO 設備無法滿足互聯網應用海量的讀寫請求的問題。本文主要介紹了淺談Redis 緩存的三大問題及其解決方案,感興趣的可以了解一下
    2021-07-07

最新評論

山阴县| 长春市| 横峰县| 崇仁县| 巫溪县| 滨州市| 凤阳县| 山东| 永清县| 襄垣县| 平南县| 抚顺县| 泾源县| 普陀区| 安康市| 德清县| 阳西县| 淳化县| 双江| 临颍县| 西贡区| 通辽市| 望江县| 仪征市| 华阴市| 瑞昌市| 全南县| 右玉县| 会东县| 永德县| 太康县| 莱阳市| 金坛市| 宜兰市| 宁强县| 阳山县| 甘南县| 江孜县| 临湘市| 阿拉善右旗| 谢通门县|