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