Redis新版本開始引入多線程专甩,談?wù)勀愕目捶ǎ?/h1>

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ù))需要在給定的時間點(diǎn)執(zhí)行灵奖,而時間事件就是服務(wù)器對這類定時操作的抽象嚼沿,比如過期鍵清理,服務(wù)狀態(tài)統(tǒng)計等瓷患。
image
image.gif

如上圖骡尽,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ù)猿规,通過一個線程實(shí)現(xiàn)了多客戶端并發(fā)處理。
image
image.gif

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

Lazy Free機(jī)制

如上所知宝剖,Redis在處理客戶端命令時是以單線程形式運(yùn)行洁闰,而且處理速度很快,期間不會響應(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ù)理肺,最終實(shí)現(xiàn)大鍵的刪除摄闸。但是善镰,該方案可能會導(dǎo)致回收速度趕不上創(chuàng)建速度,最終導(dǎo)致內(nèi)存耗盡年枕。因此炫欺,Redis最終實(shí)現(xiàn)上是將大鍵的刪除操作異步化,采用非阻塞刪除(對應(yīng)命令UNLINK)熏兄,大鍵的空間回收交由單獨(dú)線程實(shí)現(xiàn)品洛,主線程只做關(guān)系解除,可以快速返回摩桶,繼續(xù)處理其他事件桥状,避免服務(wù)器長時間阻塞。

以刪除(DEL命令)為例硝清,看看Redis是如何實(shí)現(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);
}

image.gif

同步刪除很簡單变汪,只要把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;
    }
}

image.gif

通過引入a threaded lazy free勒庄,Redis實(shí)現(xiàn)了對于Slow Operation的Lazy操作串前,避免了在大鍵刪除瘫里,F(xiàn)LUSHALL,F(xiàn)LUSHDB時導(dǎo)致服務(wù)器阻塞荡碾。當(dāng)然谨读,在實(shí)現(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é)點(diǎn)value的不同實(shí)現(xiàn)劳景。

// 3.2.5版本ZSet節(jié)點(diǎn)實(shí)現(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é)點(diǎn)實(shí)現(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;

image.gif

去掉共享對象盟广,不但實(shí)現(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版本實(shí)現(xiàn)了多線程I/O呀酸。

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

如上圖紅色部分,就是Redis實(shí)現(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 == 0) break;
    }

    ...

    return processed;
}

image.gif

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] Donen", id);
    }
}

image.gif

局限性

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

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

相較于6.0版本的多線程,Tair的多線程實(shí)現(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 Thread和Worker Thread之間采用無鎖隊列和管道進(jìn)行數(shù)據(jù)交換嗓化,整體性能會更好棠涮。
image
image.gif

小結(jié)

Redis 4.0引入Lazy Free線程,解決了諸如大鍵刪除導(dǎo)致服務(wù)器阻塞問題刺覆,在6.0版本引入了I/O Thread線程严肪,正式實(shí)現(xiàn)了多線程,但相較于Tair,并不太優(yōu)雅驳糯,而且性能提升上并不多篇梭,壓測看,多線程版本性能是單線程版本的2倍酝枢,Tair多線程版本則是單線程版本的3倍恬偷。在作者看來,Redis多線程無非兩種思路帘睦,I/O threading和Slow 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實(shí)現(xiàn)的更加完善,采用Module實(shí)現(xiàn)對慢操作的優(yōu)化赤兴,著實(shí)值得期待妖滔。

推薦閱讀

為什么阿里巴巴的程序員成長速度這么快,看完他們的成長筆記我懂了

【算法篇】Leetcode刷題的50個小技巧桶良,你居然不知道座舍?

看完三件事??

如果你覺得這篇內(nèi)容對你還蠻有幫助,我想邀請你幫我三個小忙:

點(diǎn)贊陨帆,轉(zhuǎn)發(fā)曲秉,有你們的 『點(diǎn)贊和評論』,才是我創(chuàng)造的動力疲牵。

關(guān)注公眾號 『 Java斗帝 』承二,不定期分享原創(chuàng)知識。

同時可以期待后續(xù)文章ing??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者

  • 序言:七十年代末纲爸,一起剝皮案震驚了整個濱河市亥鸠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌识啦,老刑警劉巖负蚊,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異颓哮,居然都是意外死亡家妆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門冕茅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伤极,“玉大人蛹找,你說我怎么就攤上這事∩谄海” “怎么了庸疾?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長齿税。 經(jīng)常有香客問我彼硫,道長炊豪,這世上最難降的妖魔是什么凌箕? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮词渤,結(jié)果婚禮上牵舱,老公的妹妹穿的比我還像新娘。我一直安慰自己缺虐,他們只是感情好芜壁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著高氮,像睡著了一般慧妄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剪芍,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天塞淹,我揣著相機(jī)與錄音,去河邊找鬼罪裹。 笑死饱普,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的状共。 我是一名探鬼主播套耕,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼峡继!你這毒婦竟也來了冯袍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤碾牌,失蹤者是張志新(化名)和其女友劉穎康愤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體小染,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翘瓮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了裤翩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片资盅。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡调榄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呵扛,到底是詐尸還是另有隱情每庆,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布今穿,位于F島的核電站缤灵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蓝晒。R本人自食惡果不足惜腮出,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芝薇。 院中可真熱鬧胚嘲,春花似錦、人聲如沸洛二。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晾嘶。三九已至妓雾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垒迂,已是汗流浹背械姻。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留娇斑,地道東北人策添。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像毫缆,于是被迫代替她去往敵國和親唯竹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容