您好,登錄后才能下訂單哦!
這篇文章主要講解了“單線程Redis為什么這么快”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“單線程Redis為什么這么快”吧!
前言
通常來說Redis是單線程,主要是指redis的網絡IO和讀寫鍵值對是由一個線程完成的。這也是redis對外提供鍵值存儲服務的主要流程。但是其它功能,比如持久化,集群數據同步等,其實是由額外的線程執行的。
所以,redis并不是完全意義上的單線程,只是一般把它成為單線程高性能的典型代表。那么,很多小伙伴會提問,為什么用單線程?為什么單線程能這么快。
Redis為什么用單線程
首先我們要得了解下多線程的開銷問題。平時寫程序很多人都覺得使用多線程,可以增加系統吞吐率,或者增加系統擴展性。的確對于一個多線程的系統來說,在合理的資源分配情況下,確實可以增加系統中處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。但是,如果沒有良好的系統設計經驗,實際得到的結果,其實會剛開始增加線程數時,系統吞吐率會增加。但是,再進一步增加線程時,系統吞吐率就增加遲緩了,甚至會出現下降的情況。
為什么會出現這種情況呢?關鍵的性能瓶頸就是系統中多線程同時對臨界資源的訪問。比如當有多個線程要修改一個共享的數據,為了保證資源的正確性,就需要類似互斥鎖這樣額外的機制才能保證,這個額外的機制是需要額外開銷的。比如,redis有個數據類型List,它提供出隊(LPOP)和入隊(LPUSH)操作。假設redis采用多線程設計。現在假設有兩個線程T1和T2線程T1對一個List執行LPUSH操作,線程T2對該List執行LPOP操作,并對隊列長度減1。為了保證隊列長度的正確性,需要讓這兩個線程的LPUSH和LPOP串行執行,否則,我們可能就會得到錯誤的長度結果。這就是多線程編程經常會遇到的共享資源并發訪問控制問題。
而且多線程開發中,并發控制一直是多線程開發的難點問題。如果沒有設計經驗,只是簡單地采用一個粗粒度的互斥鎖,就會出現不理想的結果那就是即使增加了線程,大部分線程也在等待獲取訪問臨界資源的互斥鎖,造成并行變串行,系統吞吐率并沒有隨著線程的增加而增加。
單線程Redis為什么這么快
通常單線程的處理能力要比多線程差很多,但是Redis卻能用單線程模型達到每秒種十萬級別的處理能力。為什么呢?
一方面,Redis的大部分操作都在內存上完成,再加上它采用了高效的數據結構(比如哈希表、跳表)。
另一方面,Redis采用了多路復用機制,能在網絡IO操作中能并發處理大量的客戶端請求,從而實現高吞吐率。那么Redis為什么要采用多路復用呢?
如上圖所示,Redis為了處理一個get請求流程如下,需要監聽客戶端請求(bind/listen),然后和客戶端建立連接(accept)。從socket中讀取請求(recv),解析客戶端發送請求后,根據請求類型讀取鍵值數據(get),最后將結果返回給客戶端(send)。其中accept()和recv()默認是阻塞操作。當Redis監聽一個客戶端有連接請求,但是一直未能成功建立連接時就會阻塞在accept()函數,這樣容易導致其它客戶端無法和Redis建立連接。同樣,當Redist通過recv()從一個客戶端讀取數據時,如果數據一直沒有到達,Redis也會阻塞在recv()。所以,這都會造成Redis整個線程阻塞,無法處理其它客戶端請求,效率極低。因此,需要將socket設置為非阻塞。
Redis的非阻塞模式
socket網絡模型的非阻塞模式設置。一般主要調用fcntl。示例代碼如下
int flag flags = fcntl(fd, F_GETFL, 0); if(flags < 0) { ... } flags |= O_NONBLOCK; if(fcntl(fd, F_SETFL, flags) < 0) { ... return -1; }
在Redis的anet.c文件中也是的非阻塞代碼也是類似邏輯。用anetSetBlock函數處理,函數定義如下:
int anetSetBlock(char *err, int fd, int non_block) { int flags; /* Set the socket blocking (if non_block is zero) or non-blocking. * Note that fcntl(2) for F_GETFL and F_SETFL can't be * interrupted by a signal. */ if ((flags = fcntl(fd, F_GETFL)) == -1) { anetSetError(err, "fcntl(F_GETFL): %s", strerror(errno)); return ANET_ERR; } /* Check if this flag has been set or unset, if so, * then there is no need to call fcntl to set/unset it again. */ if (!!(flags & O_NONBLOCK) == !!non_block) return ANET_OK; if (non_block) flags |= O_NONBLOCK; else flags &= ~O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { anetSetError(err, "fcntl(F_SETFL,O_NONBLOCK): %s", strerror(errno)); return ANET_ERR; } return ANET_OK;}
監聽套接字設置為非阻塞模式,Redis調動accept()函數但一直未有連接請求到達時,Redis線程可以返回處理其它操作,而不用一直等待。類似的,也可以針對已連接套接字設置非阻塞模式,Redis調用recv()后,如果已連接套接字上一直沒有數據到達,Redis線程同樣可以返回處理其它操作。但是我們也需要有機制繼續監聽該已連接套接字,并在有數據到達時通知Redis。這樣才能保證Redis線程,即不會像基本IO模型中一直阻塞點等待,也不會導致Redis無法處理實際到達的連接請求。
基于EPOLL機制實現
Linux中的IO多路復用是指一個執行體可以同時處理多個IO流,就是經常聽到的select/EPOLL機制。該機制可以允許內核中同時允許多個監聽套接字和已連接套接字。內核會一直監聽這些套接字上的連接請求。一旦有請求到達就會交給Redis線程處理。
Redis網絡框架基于EPOLL機制,此時,Redis線程不會阻塞在某個特定的監聽或已連接套接字上,也就不會阻塞在某一個特定的客戶端請求處理上。所以,Redis可以同時處理多個客戶端的連接請求。如下圖
為了在請求到達時能通知到Redis線程,EPOLL提供了事件的回調機制。即針對不同事件調用相應的處理函數。下面我們就來介紹下它是如何實現的
Redis用如下結構體來記錄一個文件事件:
/* File event structure */typedef struct aeFileEvent { int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */ aeFileProc *rfileProc; aeFileProc *wfileProc; void *clientData;} aeFileEvent;
結構中通過mask來描述發生了什么事件:
AE_READABLE:文件描述符可讀
AE_WRITABLE:文件描述符可寫
AE_BARRIER:文件描述符阻塞
那么,回調機制怎么工作的呢?其實rfileProc和wfileProc分別就是讀事件和寫事件發生時的回調函數。它們對應的函數如下
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
Redis用如下結構體來記錄系統中注冊的事件及其狀態:
/* State of an event based program */typedef struct aeEventLoop { int maxfd; /* highest file descriptor currently registered */ int setsize; /* max number of file descriptors tracked */ long long timeEventNextId; time_t lastTime; /* Used to detect system clock skew */ aeFileEvent *events; /* Registered events */ aeFiredEvent *fired; /* Fired events */ aeTimeEvent *timeEventHead; int stop; void *apidata; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; aeBeforeSleepProc *aftersleep;} aeEventLoop;
這一結構體中,最主要的就是文件事件指針events和時間事件頭指針timeEventHead。文件事件指針event指向一個固定大小(可配置)數組,通過文件描述符作為下標,可以獲取文件對應的事件對象。
aeApiAddEvent函數
這個函數主要用來關聯事件到EPOLL,所以會調用epoll的ctl方法定義如下:
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { aeApiState *state = eventLoop->apidata; struct epoll_event ee = {0}; /* avoid valgrind warning */ /* If the fd was already monitored for some event, we need a MOD * operation. Otherwise we need an ADD operation. * * 如果 fd 沒有關聯任何事件,那么這是一個 ADD 操作。 * 如果已經關聯了某個/某些事件,那么這是一個 MOD 操作。 */ int op = eventLoop->events[fd].mask == AE_NONE ? EPOLL_CTL_ADD : EPOLL_CTL_MOD; ee.events = 0; mask |= eventLoop->events[fd].mask; /* Merge old events */ if (mask & AE_READABLE) ee.events |= EPOLLIN; if (mask & AE_WRITABLE) ee.events |= EPOLLOUT; ee.data.fd = fd; if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1; return 0;}
當Redis服務創建一個客戶端請求的時候會調用,會注冊一個讀事件。
當Redis需要給客戶端寫數據的時候會調用prepareClientToWrite。這個方法主要是注冊對應fd的寫事件。
如果注冊失敗,Redis就不會將數據寫入緩沖。
如果對應套件字可寫,那么Redis的事件循環就會將緩沖區新數據寫入socket。
事件注冊函數aeCreateFileEvent
這個是文件事件的注冊過程,函數實現如下
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData){ if (fd >= eventLoop->setsize) { errno = ERANGE; return AE_ERR; } aeFileEvent *fe = &eventLoop->events[fd]; if (aeApiAddEvent(eventLoop, fd, mask) == -1) return AE_ERR; fe->mask |= mask; if (mask & AE_READABLE) fe->rfileProc = proc; if (mask & AE_WRITABLE) fe->wfileProc = proc; fe->clientData = clientData; if (fd > eventLoop->maxfd) eventLoop->maxfd = fd; return AE_OK;}
這個函數首先根據文件描述符獲得文件事件對象,接著在操作系統中添加自己關心的文件描述符(利用上面提到的addApiAddEvent函數),最后將回調函數記錄到文件事件對象中。因此,一個線程就可以同時監聽多個文件事件,這就是IO多路復用了。
aeMain函數
Redis事件處理器的主循環
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { //開始處理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS| AE_CALL_BEFORE_SLEEP| AE_CALL_AFTER_SLEEP); }}
這個方法最終會調用epoll_wait()獲取對應事件并執行。
這些事件會放進一個事件隊列,Redis單線程會對該事件隊列不斷進行處理。比如當有讀請求到達時,讀請求對應讀事件。Redis對這個事件注冊get回調函數。當內核監聽到有讀請求到達時,就會觸發讀事件,這個時候就會回調Redis相應的get函數。
向客戶端返回數據
Redis完成請求后,Redis并非處理完一個請求后就注冊一個寫文件事件,然后事件回調函數中往客戶端寫回結果。檢測到文件事件發生后,Redis對這些文件事件進行處理,即調用rReadProc或writeProc回調函數。處理完成后,對于需要向客戶端寫回的數據,先緩存到內存中。
typedef struct client { ... list *reply; /* List of reply objects to send to the client. */ ... int bufpos; char buf[PROTO_REPLY_CHUNK_BYTES];} client;
發送給客戶端的數據會存放到兩個地方:
reply指針存放待發送的對象;
buf中存放待返回的數據,bufpos指示數據中的最后一個字節所在位置。
注意:只要能存放在buf中,就盡量存入buf字節數組中,如果buf存不下了,才存放在reply對象數組中。
寫回客戶端發生在進入下一次等待文件事件之前,會調用以下函數處理寫回邏輯
int writeToClient(int fd, client *c, int handler_installed) { while(clientHasPendingReplies(c)) { if (c->bufpos > 0) { nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen); if (nwritten <= 0) break; c->sentlen += nwritten; totwritten += nwritten; if ((int)c->sentlen == c->bufpos) { c->bufpos = 0; c->sentlen = 0; } } else { o = listNodeValue(listFirst(c->reply)); objlen = o->used; if (objlen == 0) { c->reply_bytes -= o->size; listDelNode(c->reply,listFirst(c->reply)); continue; } nwritten = write(fd, o->buf + c->sentlen, objlen - c->sentlen); if (nwritten <= 0) break; c->sentlen += nwritten; totwritten += nwritten; } }}
感謝各位的閱讀,以上就是“單線程Redis為什么這么快”的內容了,經過本文的學習后,相信大家對單線程Redis為什么這么快這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。