电竞比分网-中国电竞赛事及体育赛事平台

分享

Redis為什么這么快?

 新進(jìn)小設(shè)計 2022-02-19

| 作者 吳顯堅,騰訊云數(shù)據(jù)庫高級工程師,參與過360開源項目Pika的研發(fā)工作,現(xiàn)從事redis數(shù)據(jù)庫研發(fā)工作。


Redis服務(wù)器是一個事件驅(qū)動程序, 事件是Redis服務(wù)器的核心, 它處理兩項重要的任務(wù), 一個是IO事件(文件事件), 另外一個是時間事件. Redis服務(wù)器通過套接字與客戶端進(jìn)行連接, 而文件事件可以理解為服務(wù)器對套接字操作的抽象. 服務(wù)器與客戶端的通信會產(chǎn)生相應(yīng)的文件事件, 而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作. 另外Redis內(nèi)部有一些操作(從Redis4.0的代碼分析目前時間事件只有serverCron)需要在給定的時間點執(zhí)行, 而時間事件就是Redis服務(wù)器對這類定時操作的抽象。

一、aeEventLoop

在分析具體代碼之前, 我們先了解一下在事件處理中處于核心部分的aeEventLoop到底是什么:

/* State of an event based program */

創(chuàng)建aeEventLoop只需要一個setsize參數(shù), 它標(biāo)識了當(dāng)前aeEventLoop最大可以監(jiān)聽的文件描述符數(shù)(通常redis傳入server.maxclients+CONFIG_FDSET_INCR,也就是在用戶指定的最大客戶端連接數(shù)的基礎(chǔ)上再額外增加128, 這128可以用于Redis內(nèi)部打開AOF,RDB文件以及主從, 集群互相通信所對應(yīng)的文件句柄), 創(chuàng)建aeEventLoop時, aeFileEvent和aeFiredEvent數(shù)組的大小就由setsize確定。

1. aeFileEvent

內(nèi)部以掩碼的形式存儲了當(dāng)前套接字關(guān)心的事件(可讀/可寫事件), 內(nèi)部還有兩個函數(shù)指針指向可讀/可寫事件發(fā)生時應(yīng)該調(diào)用的函數(shù), 另外還有一個無類型的指針指向相關(guān)聯(lián)的數(shù)據(jù), 這里需要注意的是, events是一個數(shù)組, 而套接字就是作為下標(biāo)來進(jìn)行索引對應(yīng)aeFileEvent, 例如我當(dāng)前關(guān)心的套接字是9, 那么events[9]就是它對應(yīng)的文件事件數(shù)據(jù)結(jié)構(gòu)(csapp中提到過, 當(dāng)我們調(diào)用系統(tǒng)函數(shù)返回描述符數(shù)字時, 返回的描述符總是在進(jìn)程中當(dāng)前沒有打開的最小描述符, 所以我們無需擔(dān)心文件描述符被反復(fù)的創(chuàng)建銷毀, 而越來越大的問題)。

2. aeFiredEvent

內(nèi)部以掩碼的形式存儲了當(dāng)前已經(jīng)觸發(fā)的事件和對應(yīng)的套接字, 實際上fired數(shù)組只有在調(diào)用aeApiPoll的時候才會被賦值, 例如當(dāng)前發(fā)現(xiàn)有套接字6, 8有可讀事件, 而套接字10有可寫事件, 那么fired數(shù)組的前三個元素會被賦值{fd = 6, mask =AE_READABLE}, {fd = 8, mask = AE_READABLE}, {fd = 10, mask = AE_WRITABLE}, 緊接著我們以6為索引, 找到文件事件數(shù)據(jù)結(jié)構(gòu)events[6],然后發(fā)現(xiàn)觸發(fā)的是可讀事件, 我們再調(diào)用events[6]中rfileProc來處理可讀事件。

aeEventLoop *aeCreateEventLoop(int setsize){

對于時間事件, aeEventLoop中有一個timeEventHead指針指向第一個時間事件, 由于aeEventLoop創(chuàng)建之初, 內(nèi)部沒有任何時間事件, 所以初始化時timeEventHead指向NULL, 每當(dāng)有新的時間事件時, 總會被添加到timeEventHead頭部, 由于aeTimeEvent結(jié)構(gòu)體中有next指針可以指向下一個aeTimeEvent結(jié)構(gòu)體, 所以只要我們獲取timeEventHead就能遍歷當(dāng)前所有的時間事件了, 另外有一個細(xì)節(jié)需要注意, 最后一個aeTimeEvent結(jié)構(gòu)體中的next指針指向的是timeEventHead, 所以所有時間事件實際上就是由一個環(huán)形鏈表串連起來的。

image.png

二、文件事件

在介紹中有提到過文件事件實際上就是服務(wù)器對套接字操作的抽象, 當(dāng)套接字有可讀\寫事件觸發(fā)的時候, 我們需要調(diào)用相應(yīng)的處理函數(shù), 下面先看一下跟文件事件相關(guān)的結(jié)構(gòu)體:

/* File event structure */

在aeEventLoop初始化的時候會為aeFileEvent數(shù)組(events)分配空間, 數(shù)組的大小由參數(shù)setsize指定,表明了當(dāng)前Redis最大打開的套接字的大小, 套接字與aeFileEvent一一對應(yīng), 也就是說我們可以通過套接字?jǐn)?shù)值作為索引到events數(shù)組中找到他對應(yīng)的aeFileEvent對象。

當(dāng)我們在aeEventLoop中注冊一個文件事件時, 首先我們判斷傳入的套接字對events數(shù)組是否有越界行為, 若沒有越界行為, 我們便可以獲取與當(dāng)前套接字對應(yīng)的aeFileEvent對象, 然后調(diào)用aeApiAddEvent將當(dāng)前的文件描述符以及監(jiān)聽的事件注冊到底層IO多路復(fù)用機制(epoll, select, evport, kqueue其中之一)中, 另外我們還需要指定當(dāng)可讀/可寫事件發(fā)生時需要調(diào)用的函數(shù),另外當(dāng)前文件事件的一些私有數(shù)據(jù)被存放在clientData指向的對象當(dāng)中。

int aeCreateFileEvent(aeEventLoop *eventLoop,int fd, int mask,

三、時間事件

Redis內(nèi)部的時間事件實際可以分為兩類, 一類是定時事件, 也就是需要在未來某一個時間點觸發(fā)的事件(只觸發(fā)一次), 另外一類是周期性事件,和前面的定時事件只觸發(fā)一次不同, 周期性事件是每隔一段時間又會重新觸發(fā)一次。

Redis使用了timeProc指向函數(shù)的返回值來判斷當(dāng)前屬于哪類事件, 若函數(shù)返回AE_NOMORE(也就是-1),說明當(dāng)前事件無需再次觸發(fā)(將id置刪除標(biāo)記AE_DELETED_EVENT_ID), 若函數(shù)返回一個大于等于0的值n, 說明再等待n秒, 該事件需要再重新被觸發(fā)(根據(jù)返回值更新when_sec和when_ms),在博客開頭提到的serverCron時間事件實際上就是一個周期性事件, 函數(shù)末尾會返回1000/server.hz, server.hz默認(rèn)被設(shè)置為10, 也就是說serverCron平均每間隔100ms會被調(diào)用一次。

/* Time event structure */

Redis調(diào)用aeCreateTimeEvent來創(chuàng)建一個時間任務(wù), 實現(xiàn)非常簡單, 傳參我們關(guān)注一下milliseconds和proc即可, 前者指定了時間事件距離當(dāng)前的觸發(fā)時間, 后者指定了時間事件觸發(fā)時應(yīng)調(diào)用的函數(shù), 內(nèi)部通過aeAddMillisecondsToNow將當(dāng)前定時任務(wù)觸發(fā)的時間戳計算出來賦值給when_sec和when_ms, 然后再將timeProc指向時間事件到達(dá)時應(yīng)該調(diào)用的函數(shù)。

在完成了aeTimeEvent結(jié)構(gòu)體內(nèi)部變量賦值之后, 最后將其添加到aeEventLoop內(nèi)部的存儲定時間事件的環(huán)形鏈表的頭部中(這里需要注意的是, 由于我們總是將新的時間事件加入環(huán)形鏈表的頭部, 所以時間事件觸發(fā)的時間先后并不是在環(huán)形鏈表中有序的, 我們需要將環(huán)形鏈表遍歷完畢才能保證當(dāng)前已經(jīng)到達(dá)的時間事件都已經(jīng)被處理完畢, 不過由于在開頭提到過, 目前Redis只存在serverCron一個時間事件, 所以我們無需擔(dān)心遍歷環(huán)形鏈表影響服務(wù)性能), 此時一個時間事件就算創(chuàng)建完成了。

static void aeAddMillisecondsToNow(long longmilliseconds, long *sec, long *ms) {

Redis通過aeDeleteTimeEvent函數(shù)來刪除一個時間任務(wù), 傳參只有一個待刪除時間事件的id, 我們發(fā)現(xiàn)這里的刪除實際上是一種惰性刪除, 將aeTimeEvent中的id標(biāo)記為AE_DELETED_EVENT_ID, 而不是直接將aeTimeEvent對象從鏈表中刪除并且釋放, 個人認(rèn)為這么實現(xiàn)的原因更多是為了安全考慮以及代碼的簡潔性, 考慮在一個時間事件中本來想刪除另外一個時間事件, 但是由于id填錯, 誤刪成自己了, 此時如果釋放自身aeTimeEvent對象, 這是十分危險的。

int aeDeleteTimeEvent(aeEventLoop *eventLoop,long long id)

四、事件的調(diào)度與執(zhí)行

Redis是單線程的, 內(nèi)部是一直處于aeMain中的while循環(huán)中, 而循環(huán)內(nèi)部不斷調(diào)用aeProcessEvents函數(shù), 該函數(shù)會對上面提到的文件事件和時間事件進(jìn)行調(diào)度, 決定何時處理文件事件以及時間事件。

void aeMain(aeEventLoop *eventLoop) {

實際上aeProcessEvents函數(shù)內(nèi)部做的事情也非常簡單, 下面進(jìn)行了梳理:

1. 首先調(diào)用aeSearchNearestTimer獲取到達(dá)時間距離當(dāng)前最近的時間事件;

2. 計算上一步獲取到的時間事件還有多久才可以觸發(fā), 并且將結(jié)果記錄到一個struct timeval*指針指向的結(jié)構(gòu)體中(若在步驟一中沒有獲取到時間事件對象, 那么指針為NULL);

3. 阻塞并等待文件事件的產(chǎn)生, 最大的阻塞時間由步驟二決定(步驟二指針為NULL的場景表示當(dāng)前沒有時間事件, 我們可以永遠(yuǎn)阻塞, 直到有文件事件到達(dá));

4. 如果在最大阻塞時間內(nèi)獲取到了文件事件, 則根據(jù)文件事件的類型調(diào)用對應(yīng)的讀事件處理函數(shù)或者寫事件處理函數(shù);

5. 遍歷時間事件鏈表, 在這個過程中可能會遇到id為AE_DELETED_EVENT_ID的代表已經(jīng)做了刪除標(biāo)記的時間事件, 需要將該時間事件從鏈表中移除, 并且進(jìn)行釋放, 如遇到已經(jīng)達(dá)到的時間事件, 則調(diào)用其綁定的處理函數(shù), 并且根據(jù)返回值來判斷該事件時間是否需要在給定的時間內(nèi)再重新觸發(fā)。

五、問題

Q1: 時間事件觸發(fā)的時間一定精準(zhǔn)么?

A1: 時間事件的觸發(fā)并不能在指定的時間精準(zhǔn)觸發(fā), 一般都要比指定的時間稍晚一點, 此外在Redis單線程模型下, 時間事件都是串行執(zhí)行的, 中間如果某個時間事件處理時間長, 更加影響了后面時間事件執(zhí)行時間的精準(zhǔn)性. 而且時間事件鏈表是無序的, 所以在極端場景下, 存在優(yōu)先級低的時間事件比優(yōu)先級高的事件先觸發(fā)的可能性, 不過好在目前Redis內(nèi)部只有一個時間事件, 所以影響不會太大.


Q2: aeEventLoop在創(chuàng)建之初就指定了可監(jiān)聽文件描述符的數(shù)量, 之后又通過config set maxclients命令動態(tài)調(diào)整客戶端最大連接數(shù)是怎么實現(xiàn)的?

A2: 通過翻看源碼了解到, aeEventLoop提供了aeResizeSetSize函數(shù), 用戶重新分配events和fired數(shù)組的大小, 使aeEventLoop可監(jiān)聽的套接字?jǐn)?shù)量得以調(diào)整, 當(dāng)新的maxclients比原先要大的時候, 會調(diào)用該函數(shù), 擴大aeEventLoop可監(jiān)聽文件描述符的數(shù)量, 以支持更多的客戶端連接.

int aeResizeSetSize(aeEventLoop *eventLoop,int setsize) {

六、總結(jié)

Redis對事件的處理方式十分巧妙, 文件事件和時間事件之間相互配合, 充分的利用時間事件達(dá)到之前的這段時間等待和處理文件事件, 這樣既避免了CPU的空轉(zhuǎn)檢查, 也能及時的處理文件事件. 此外通過時間事件中timeProc函數(shù)的返回值, 將時間事件的移除和再次觸發(fā)權(quán)完全交給了用戶, 使用起來更加靈活.

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布!

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多