1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Redis單線程已經(jīng)很快了,為什么6.0要引入多線程?帶來什么優(yōu)勢?

        共 15926字,需瀏覽 32分鐘

         ·

        2021-03-30 12:15


        來源 | r6d.cn/b8b7


        Redis作為一個基于內(nèi)存的緩存系統(tǒng),一直以高性能著稱,因沒有上下文切換以及無鎖操作,即使在單線程處理情況下,讀速度仍可達(dá)到11萬次/s,寫速度達(dá)到8.1萬次/s。但是,單線程的設(shè)計也給Redis帶來一些問題:
        • 只能使用CPU一個核;
        • 如果刪除的鍵過大(比如Set類型中有上百萬個對象),會導(dǎo)致服務(wù)端阻塞好幾秒;
        • QPS難再提高。

        針對上面問題,Redis在4.0版本以及6.0版本分別引入了Lazy Free以及多線程IO,逐步向多線程過渡,下面將會做詳細(xì)介紹。

        單線程原理

        都說Redis是單線程的,那么單線程是如何體現(xiàn)的?如何支持客戶端并發(fā)請求的?為了搞清這些問題,首先來了解下Redis是如何工作的。

        Redis服務(wù)器是一個事件驅(qū)動程序,服務(wù)器需要處理以下兩類事件:

        • 文件事件:Redis服務(wù)器通過套接字與客戶端(或者其他Redis服務(wù)器)進(jìn)行連接,而文件事件就是服務(wù)器對套接字操作的抽象;服務(wù)器與客戶端的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作,比如連接accept,read,write,close等;時間事件:Redis服務(wù)器中的一些操作(比如serverCron函數(shù))需要在給定的時間點執(zhí)行,而時間事件就是服務(wù)器對這類定時操作的抽象,比如過期鍵清理,服務(wù)狀態(tài)統(tǒng)計等。

        如上圖,Redis將文件事件和時間事件進(jìn)行抽象,時間輪訓(xùn)器會監(jiān)聽I/O事件表,一旦有文件事件就緒,Redis就會優(yōu)先處理文件事件,接著處理時間事件。在上述所有事件處理上,Redis都是以單線程形式處理,所以說Redis是單線程的。此外,如下圖,Redis基于Reactor模式開發(fā)了自己的I/O事件處理器,也就是文件事件處理器,Redis在I/O事件處理上,采用了I/O多路復(fù)用技術(shù),同時監(jiān)聽多個套接字,并為套接字關(guān)聯(lián)不同的事件處理函數(shù),通過一個線程實現(xiàn)了多客戶端并發(fā)處理。

        正因為這樣的設(shè)計,在數(shù)據(jù)處理上避免了加鎖操作,既使得實現(xiàn)上足夠簡潔,也保證了其高性能。當(dāng)然,Redis單線程只是指其在事件處理上,實際上,Redis也并不是單線程的,比如生成RDB文件,就會fork一個子進(jìn)程來實現(xiàn),當(dāng)然,這不是本文要討論的內(nèi)容。

        Lazy Free機制

        如上所知,Redis在處理客戶端命令時是以單線程形式運行,而且處理速度很快,期間不會響應(yīng)其他客戶端請求,但若客戶端向Redis發(fā)送一條耗時較長的命令,比如刪除一個含有上百萬對象的Set鍵,或者執(zhí)行flushdb,flushall操作,Redis服務(wù)器需要回收大量的內(nèi)存空間,導(dǎo)致服務(wù)器卡住好幾秒,對負(fù)載較高的緩存系統(tǒng)而言將會是個災(zāi)難。為了解決這個問題,在Redis 4.0版本引入了Lazy Free,將慢操作異步化,這也是在事件處理上向多線程邁進(jìn)了一步。

        如作者在其博客中所述,要解決慢操作,可以采用漸進(jìn)式處理,即增加一個時間事件,比如在刪除一個具有上百萬個對象的Set鍵時,每次只刪除大鍵中的一部分?jǐn)?shù)據(jù),最終實現(xiàn)大鍵的刪除。但是,該方案可能會導(dǎo)致回收速度趕不上創(chuàng)建速度,最終導(dǎo)致內(nèi)存耗盡。因此,Redis最終實現(xiàn)上是將大鍵的刪除操作異步化,采用非阻塞刪除(對應(yīng)命令UNLINK),大鍵的空間回收交由單獨線程實現(xiàn),主線程只做關(guān)系解除,可以快速返回,繼續(xù)處理其他事件,避免服務(wù)器長時間阻塞。

        以刪除(DEL命令)為例,看看Redis是如何實現(xiàn)的,下面就是刪除函數(shù)的入口,其中,lazyfree_lazy_user_del是是否修改DEL命令的默認(rèn)行為,一旦開啟,執(zhí)行DEL時將會以UNLINK形式執(zhí)行。

        void delCommand(client *c) {
            delGenericCommand(c,server.lazyfree_lazy_user_del);
        }

        /* This command implements DEL and LAZYDEL. */
        void delGenericCommand(client *c, int lazy) {
            int numdel = 0, j;

            for (j = 1; j < c->argc; j++) {
                expireIfNeeded(c->db,c->argv[j]);
                // 根據(jù)配置確定DEL在執(zhí)行時是否以lazy形式執(zhí)行
                int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                                      dbSyncDelete(c->db,c->argv[j]);
                if (deleted) {
                    signalModifiedKey(c,c->db,c->argv[j]);
                    notifyKeyspaceEvent(NOTIFY_GENERIC,
                        "del",c->argv[j],c->db->id);
                    server.dirty++;
                    numdel++;
                }
            }
            addReplyLongLong(c,numdel);
        }`

        同步刪除很簡單,只要把key和value刪除,如果有內(nèi)層引用,則進(jìn)行遞歸刪除,這里不做介紹。下面看下異步刪除,Redis在回收對象時,會先計算回收收益,只有回收收益在超過一定值時,采用封裝成Job加入到異步處理隊列中,否則直接同步回收,這樣效率更高?;厥帐找嬗嬎阋埠芎唵?,比如String類型,回收收益值就是1,而Set類型,回收收益就是集合中元素個數(shù)。

        /* Delete a key, value, and associated expiration entry if any, from the DB.
         * If there are enough allocations to free the value object may be put into
         * a lazy free list instead of being freed synchronously. The lazy free list
         * will be reclaimed in a different bio.c thread. */

        #define LAZYFREE_THRESHOLD 64
        int dbAsyncDelete(redisDb *db, robj *key) {
            /* Deleting an entry from the expires dict will not free the sds of
             * the key, because it is shared with the main dictionary. */

            if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

            /* If the value is composed of a few allocations, to free in a lazy way
             * is actually just slower... So under a certain limit we just free
             * the object synchronously. */

            dictEntry *de = dictUnlink(db->dict,key->ptr);
            if (de) {
                robj *val = dictGetVal(de);
                // 計算value的回收收益
                size_t free_effort = lazyfreeGetFreeEffort(val);

                /* If releasing the object is too much work, do it in the background
                 * by adding the object to the lazy free list.
                 * Note that if the object is shared, to reclaim it now it is not
                 * possible. This rarely happens, however sometimes the implementation
                 * of parts of the Redis core may call incrRefCount() to protect
                 * objects, and then call dbDelete(). In this case we'll fall
                 * through and reach the dictFreeUnlinkedEntry() call, that will be
                 * equivalent to just calling decrRefCount(). */

                // 只有回收收益超過一定值,才會執(zhí)行異步刪除,否則還是會退化到同步刪除
                if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
                    atomicIncr(lazyfree_objects,1);
                    bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
                    dictSetVal(db->dict,de,NULL);
                }
            }

            /* Release the key-val pair, or just the key if we set the val
             * field to NULL in order to lazy free it later. */

            if (de) {
                dictFreeUnlinkedEntry(db->dict,de);
                if (server.cluster_enabled) slotToKeyDel(key->ptr);
                return 1;
            } else {
                return 0;
            }
        }`

        通過引入a threaded lazy free,Redis實現(xiàn)了對于Slow OperationLazy操作,避免了在大鍵刪除,FLUSHALLFLUSHDB時導(dǎo)致服務(wù)器阻塞。當(dāng)然,在實現(xiàn)該功能時,不僅引入了lazy free線程,也對Redis聚合類型在存儲結(jié)構(gòu)上進(jìn)行改進(jìn)。因為Redis內(nèi)部使用了很多共享對象,比如客戶端輸出緩存。當(dāng)然,Redis并未使用加鎖來避免線程沖突,鎖競爭會導(dǎo)致性能下降,而是去掉了共享對象,直接采用數(shù)據(jù)拷貝,如下,在3.x和6.x中ZSet節(jié)點value的不同實現(xiàn)。

        // 3.2.5版本ZSet節(jié)點實現(xiàn),value定義robj *obj
        /* ZSETs use a specialized version of Skiplists */
        typedef struct zskiplistNode {
            robj *obj;
            double score;
            struct zskiplistNode *backward;
            struct zskiplistLevel {
                struct zskiplistNode *forward;
                unsigned int span;
            } level[];
        } zskiplistNode;

        // 6.0.10版本ZSet節(jié)點實現(xiàn),value定義為sds ele
        /* ZSETs use a specialized version of Skiplists */
        typedef struct zskiplistNode {
            sds ele;
            double score;
            struct zskiplistNode *backward;
            struct zskiplistLevel {
                struct zskiplistNode *forward;
                unsigned long span;
            } level[];
        } zskiplistNode;`

        去掉共享對象,不但實現(xiàn)了lazy free功能,也為Redis向多線程跨進(jìn)帶來了可能,正如作者所述:

        Now that values of aggregated data types are fully unshared, and client output buffers don’t contain shared objects as well, there is a lot to exploit. For example it is finally possible to implement threaded I/O in Redis, so that different clients are served by different threads. This means that we’ll have a global lock only when accessing the database, but the clients read/write syscalls and even the parsing of the command the client is sending, can happen in different threads.

        多線程I/O及其局限性

        Redis在4.0版本引入了Lazy Free,自此Redis有了一個Lazy Free線程專門用于大鍵的回收,同時,也去掉了聚合類型的共享對象,這為多線程帶來可能,Redis也不負(fù)眾望,在6.0版本實現(xiàn)了多線程I/O。

        實現(xiàn)原理

        正如官方以前的回復(fù),Redis的性能瓶頸并不在CPU上,而是在內(nèi)存和網(wǎng)絡(luò)上。因此6.0發(fā)布的多線程并未將事件處理改成多線程,而是在I/O上,此外,如果把事件處理改成多線程,不但會導(dǎo)致鎖競爭,而且會有頻繁的上下文切換,即使用分段鎖來減少競爭,對Redis內(nèi)核也會有較大改動,性能也不一定有明顯提升。

        如上圖紅色部分,就是Redis實現(xiàn)的多線程部分,利用多核來分擔(dān)I/O讀寫負(fù)荷。在事件處理線程每次獲取到可讀事件時,會將所有就緒的讀事件分配給I/O線程,并進(jìn)行等待,在所有I/O線程完成讀操作后,事件處理線程開始執(zhí)行任務(wù)處理,在處理結(jié)束后,同樣將寫事件分配給I/O線程,等待所有I/O線程完成寫操作。

        以讀事件處理為例,看下事件處理線程任務(wù)分配流程:

        int handleClientsWithPendingReadsUsingThreads(void) {
            ...

            /* Distribute the clients across N different lists. */
            listIter li;
            listNode *ln;
            listRewind(server.clients_pending_read,&li);
            int item_id = 0;
            // 將等待處理的客戶端分配給I/O線程
            while((ln = listNext(&li))) {
                client *c = listNodeValue(ln);
                int target_id = item_id % server.io_threads_num;
                listAddNodeTail(io_threads_list[target_id],c);
                item_id++;
            }

            ...

            /* Wait for all the other threads to end their work. */
            // 輪訓(xùn)等待所有I/O線程處理完
            while(1) {
                unsigned long pending = 0;
                for (int j = 1; j < server.io_threads_num; j++)
                    pending += io_threads_pending[j];
                if (pending == 0break;
            }

            ...

            return processed;
        }`

        I/O線程處理流程:

        void *IOThreadMain(void *myid) {
            ...

            while(1) {
                ...

                // I/O線程執(zhí)行讀寫操作
                while((ln = listNext(&li))) {
                    client *c = listNodeValue(ln);
                    // io_threads_op判斷是讀還是寫事件
                    if (io_threads_op == IO_THREADS_OP_WRITE) {
                        writeToClient(c,0);
                    } else if (io_threads_op == IO_THREADS_OP_READ) {
                        readQueryFromClient(c->conn);
                    } else {
                        serverPanic("io_threads_op value is unknown");
                    }
                }
                listEmpty(io_threads_list[id]);
                io_threads_pending[id] = 0;

                if (tio_debug) printf("[%ld] Done\n", id);
            }
        }`

        局限性

        從上面實現(xiàn)上看,6.0版本的多線程并非徹底的多線程,I/O線程只能同時執(zhí)行讀或者同時執(zhí)行寫操作,期間事件處理線程一直處于等待狀態(tài),并非流水線模型,有很多輪訓(xùn)等待開銷。

        Tair多線程實現(xiàn)原理

        相較于6.0版本的多線程,Tair的多線程實現(xiàn)更加優(yōu)雅。如下圖,Tair的Main Thread負(fù)責(zé)客戶端連接建立等,IO Thread負(fù)責(zé)請求讀取、響應(yīng)發(fā)送、命令解析等,Worker Thread線程專門用于事件處理。IO Thread讀取用戶的請求并進(jìn)行解析,之后將解析結(jié)果以命令的形式放在隊列中發(fā)送給Worker Thread處理。Worker Thread將命令處理完成后生成響應(yīng),通過另一條隊列發(fā)送給IO Thread。為了提高線程的并行度,IO ThreadWorker Thread之間采用無鎖隊列 和管道 進(jìn)行數(shù)據(jù)交換,整體性能會更好。

        小結(jié)

        Redis 4.0引入Lazy Free線程,解決了諸如大鍵刪除導(dǎo)致服務(wù)器阻塞問題,在6.0版本引入了I/O Thread線程,正式實現(xiàn)了多線程,但相較于Tair,并不太優(yōu)雅,而且性能提升上并不多,壓測看,多線程版本性能是單線程版本的2倍,Tair多線程版本則是單線程版本的3倍。在作者看來,Redis多線程無非兩種思路,I/O threadingSlow commands threading,正如作者在其博客中所說:

        I/O threading is not going to happen in Redis AFAIK, because after much consideration I think it’s a lot of complexity without a good reason. Many Redis setups are network or memory bound actually. Additionally I really believe in a share-nothing setup, so the way I want to scale Redis is by improving the support for multiple Redis instances to be executed in the same host, especially via Redis Cluster.

        What instead I really want a lot is slow operations threading, and with the Redis modules system we already are in the right direction. However in the future (not sure if in Redis 6 or 7) we’ll get key-level locking in the module system so that threads can completely acquire control of a key to process slow operations. Now modules can implement commands and can create a reply for the client in a completely separated way, but still to access the shared data set a global lock is needed: this will go away.

        Redis作者更傾向于采用集群方式來解決I/O threading,尤其是在6.0版本發(fā)布的原生Redis Cluster Proxy背景下,使得集群更加易用。

        此外,作者更傾向于slow operations threading(比如4.0版本發(fā)布的Lazy Free)來解決多線程問題。后續(xù)版本,是否會將IO Thread實現(xiàn)的更加完善,采用Module實現(xiàn)對慢操作的優(yōu)化,著實值得期待。

        1. SpringBoot 部署 Jar 文件,瘦身優(yōu)化指南 !

        2. 發(fā)現(xiàn)個外賣漏洞,希望不要被封殺!

        3. 怎樣在 Linux 系統(tǒng)中恢復(fù)已刪除文件

        4. 推薦一個高仿微信的項目 有點屌??!

        最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

        獲取方式:點“在看”,關(guān)注公眾號并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

        文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

        謝謝支持喲 (*^__^*)

        瀏覽 28
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            丰满人妻一区二区三区免费 | 公妇乱淫完整版 | 人妻无码一区二区三区 | 精品性高朝久久久久久久 | 99re热精品 | 大鸡吧伊人 | 偷偷解开女同桌的内裤摸网站 | 美女操逼逼 | 亲子伦视频一区二区三区视频 | 免费A级毛片无码专区 |