聊聊redis單執行緒為什麼能做到高效能和io多路複用到底是個什麼鬼

啊漢發表於2020-09-08

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必須保證每個任務的處理時間不能太長。 

相關文章