中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

單線程Redis為什么這么快

發布時間:2021-10-13 10:18:26 來源:億速云 閱讀:135 作者:iii 欄目:編程語言

這篇文章主要講解了“單線程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為什么這么快

如上圖所示,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為什么這么快

為了在請求到達時能通知到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為什么這么快這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

张家口市| 英超| 皋兰县| 碌曲县| 锦屏县| 青铜峡市| 柯坪县| 牟定县| 清徐县| 北川| 绿春县| 铁力市| 宣威市| 湘潭县| 邯郸市| 连州市| 图木舒克市| 遂平县| 洛隆县| 哈尔滨市| 灵山县| 南充市| 乌拉特后旗| 梧州市| 黔江区| 泸定县| 大姚县| 阆中市| 商水县| 恩施市| 石台县| 唐山市| 大连市| 临夏市| 蓝田县| 清丰县| 沁源县| 江华| 永州市| 德安县| 邯郸县|