聊聊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必須保證每一個任務的處理時間不能太長。 
相關文章
相關標籤/搜索