Redis Server跑在單進程單線程中,接收到的命令操作都是按照順序線性執行的,即便如此,它的讀寫性能依然能達到10W+的QPS,不得不說:Redis的設計十分優秀。
爲什麼Redis的讀寫性能這麼高呢?原因有許多,我們列舉主要的三個:
1、Redis基於內存操作:
絕大部分的請求爲純粹的內存操作,而且使用hash結構存儲數據,查找和操作的時間複雜度均爲O(1)。
2、Redis數據結構簡單:
redis對數據的操作還是比較簡單的,而且redis的數據結構是專門設計的。
3、單線程-IO多路複用模型:
單線程的設計省去了很多的麻煩:比如上下文切換、資源競爭、CPU切換消耗以及各種鎖操作等等問題,而IO多路複用模型的使用更讓Redis提升了效率。
今天,我們就來聊一聊:IO多路複用模型。
IO即爲網絡I/O,多路即爲多個TCP連接,複用即爲共用一個線程或者進程,模型最大的優勢是系統開銷小,不必創建也不必維護過多的線程或進程。
IO多路複用是經典的Reactor設計模式,有時也稱爲異步阻塞IO(異步指socket爲non-blocking,堵塞指select堵塞),爲常見的四種IO模型之一,其他三種分別是:同步堵塞IO、同步非堵塞IO、異步(非堵塞)IO。
IO多路複用的核心是可以同時處理多個連接請求,爲此使用了兩個系統調用,分別是:
1.select/poll/epoll--模型機制:可以監視多個描述符(fd),一旦某個描述符就緒(讀/寫/異常)就能通知程序進行相應的讀寫操作。讀寫操作都是自己負責的,也即是阻塞的,所以本質上都是同步(堵塞)IO。Redis支持這三種機制,默認使用epoll機制。2.recvfrom--接收數據。而blocking IO只調用了recvfrom,所以在連接數不高的情況下,blocking IO的性能不一定比IO多路複用差。
補充:異步IO無需自己負責讀寫操作。
1、select機制:
select原理:
當client連接到server後,server會創建一個該連接的描述符fd,fd有三種狀態,分別是讀、寫、異常,存放在三個fd_set(其實就是三個long型數組)中。
#include <sys/select.h>
//select接口,調用時會堵塞
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
而select()的本質就是通過設置和檢查fd的三個fd_set來進行下一步處理。
select大體執行步驟:
1) 使用copy_from_user從用戶空間拷貝fd_set到內核空間
2) 內核遍歷[0,nfds)範圍內的每個fd,調用fd所對應的設備的驅動poll函數,poll函數可以檢測fd的可用流(讀流、寫流、異常流)。
3) 檢查是否有流發生,如果有發生,把流設置對應的類別,並執行4,如果沒有流發生,執行5。或者timeout=0,執行4。
4) select返回。
5)select阻塞進程,等待被流對應的設備喚醒時執行2,或timeout到期,執行4。
select的侷限性:
2、poll機制:
poll原理:
poll的實現和select非常相似,輪詢+遍歷+根據描述符的狀態處理,只是fd_set換成了pollfd,而且去掉了最大描述符數量限制,其他的侷限性同樣存在。
#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 實際發生了的事件 */
} ;
3、epoll機制:epoll對select和poll進行了改進,避免了上述三個侷限性。
epoll原理:
與select和poll只提供一個接口函數不同的是,epoll提供了三個接口函數及一些結構體:
/*建一個epoll句柄*/
int epoll_create(int size);
/*向epoll句柄中添加需要監聽的fd和時間event*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*返回發生事件的隊列*/
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
struct eventpoll
{
...
/*紅黑樹的根節點,這棵樹中存儲着所有添加到epoll中的事件,也就是這個epoll_ctl監控的事件*/
struct rb_root rbr;
/*雙向鏈表rdllist保存着將要通過epoll_wait返回給用戶的、滿足條件的事件*/
struct list_head rdllist;
1.調用epoll_create:linux內核會在epoll文件系統創建一個file節點,同時創建一個eventpoll結構體,結構體中有兩個重要的成員:rbr是一棵紅黑樹,用於存放epoll_ctl註冊的socket和事件;rdllist是一條雙向鏈表,用於存放準備就緒的事件供epoll_wait調用。
2.調用epoll_ctl:會檢測rbr中是否已經存在節點,有就返回,沒有則新增,同時會向內核註冊回調函數ep_poll_callback,當有事件中斷來臨時,調用回調函數向rdllist中插入數據,epoll_ctl也可以增刪改事件。
3.調用epoll_wait:返回或者判斷rdllist中的數據即可。
epoll兩種工作模式:LT--水平觸發 ET--邊緣觸發
LT:只要文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作。
ET:檢測到有IO事件時,通過epoll_wait調用會得到有事件通知的文件描述符,對於每一個被通知的文件描述符,必須將該文件描述符一直讀到空,讓errno返回EAGAIN爲止,否則下次的epoll_wait不會返回餘下的數據,會丟掉事件。
ET比LT更加高效,因爲ET只通知一次,而LT會通知多次,LT可能會充斥大量不關心的就緒文件描述符。
epoll總結: