[redis 源碼走讀] 事件 - 定時器

定時器是 redis 異步處理事件的一個十分重要的功能。redis 定時器功能由多個時間事件組成,事件由一個雙向鏈表維護。時間事件可以處理多個定時任務。


理解 redis 定時器,我們帶着問題,看看 redis 是怎麼處理的:

  • 定時器作用是什麼。
  • 定時器實現原理。
  • 單進程裏如何同時處理文件事件和時間事件。
  • 如何實現多定時任務。

🔥文章來源:wenfh2020.com

1. 作用

定時器是 redis 異步處理任務的一個十分重要的功能。核心邏輯在 serverCron 函數裏。

  • 對設置了過期時間的數據進行檢查回收。
  • 異步回收需要關閉的鏈接(socket)。
  • 檢查 fork 的子進程是否已經關閉,處理回收的相關工作。
  • 檢查內存數據是否符合 rdb 持久化快照落地條件,fork 子進程進行快照保存。
  • bgsave rdb 生成快照或 bgrewriteaof aof 重寫延後操作。
  • 對需要擴容和縮容的哈希表(dict)進行數據遷移。
  • 集羣裏節點間的斷線重連。
  • redis 服務的一些信息統計。
  • …等等。

2. 定時器實現原理

redis 定時器功能由多個時間事件組成,事件由一個雙向鏈表維護。有些實現邏輯,上文已經提到,下面就簡單說一下部分實現原理。


2.1. 事件結構

  • 事件。
// 時間事件
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc; /* 時鐘到期事件觸發回調處理函數。*/
    aeEventFinalizerProc *finalizerProc; /* 時間事件刪除時,觸發回調。*/
    void *clientData; /* 擴展參數,異步操作方便數據回調,在 timeProc 通過參數回傳。*/
    struct aeTimeEvent *prev; /* 時間事件是一個雙向鏈表。*/
    struct aeTimeEvent *next;
} aeTimeEvent;

// 事件管理
typedef struct aeEventLoop {
    ...
    long long timeEventNextId;  // 時間事件下一個 id (通過 ‘++’ 遞增)
    ...
    aeTimeEvent *timeEventHead; // 時間事件鏈表。
    ...
} aeEventLoop;
  • 事件循環。
// 循環處理事件。
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}
  • 事件回調函數。

// 時間事件觸發處理函數。
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);

// 時間事件處理完畢,被刪除時,觸發的回調處理。
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
  • 創建時間事件,時間事件通過雙向鏈表管理,新的事件插入到鏈表頭。
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc) {
    // 事件 id 遞增。
    long long id = eventLoop->timeEventNextId++;
    aeTimeEvent *te;

    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    te->id = id;
    // 設置到期時間。
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    te->clientData = clientData;
    te->prev = NULL;
    te->next = eventLoop->timeEventHead;
    if (te->next)
        te->next->prev = te;
    eventLoop->timeEventHead = te;
    return id;
}
  • 設置事件到期時間。
static void aeAddMillisecondsToNow(long long milliseconds, long *sec, long *ms) {
    long cur_sec, cur_ms, when_sec, when_ms;

    // 當前時間增加到期時間間隔。
    aeGetTime(&cur_sec, &cur_ms);
    when_sec = cur_sec + milliseconds/1000;
    when_ms = cur_ms + milliseconds%1000;
    if (when_ms >= 1000) {
        when_sec ++;
        when_ms -= 1000;
    }
    *sec = when_sec;
    *ms = when_ms;
}

2.2. 定時器執行流程

  • redis 啓動添加時鐘處理事件。
// ae.c
int main(int argc, char **argv) {
    ...
    initServer();
    ...
    aeMain(server.el);
    ...
}

void initServer(void) {
    ...
    // 創建定時事件,綁定回調函數。
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }
    ...
}

// 時鐘回調處理函數
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
}
  • 事件循環處理。
// 循環處理事件。
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

// 處理時間事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    ...
     /* Check time events */
    if (flags & AE_TIME_EVENTS)
        // 處理時間事件
        processed += processTimeEvents(eventLoop);
    ...
}

3. 單進程異步處理事件邏輯

進程通過循環,不停地處理時間事件和文件事件。
redis 單進程處理文件事件和時間事件

  • 在進程的循環中不停地撈出文件和時間事件進行處理 aeProcessEvents
// ae.c
int main(int argc, char **argv) {
    ...
    aeMain(server.el);
    ...
}

// 循環處理事件。
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        ...
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

// 處理事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    ...
    // 先搜索出最快到期的定時器,查看時間戳,文件事件要在定時器到期前從系統內核撈出來處理。
    shortest = aeSearchNearestTimer(eventLoop);
    ...
    // 處理文件事件,等待獲取事件時間間隔不能太長,否則定時器事件處理要超時了。
    numevents = aeApiPoll(eventLoop, tvp);
    for (j = 0; j < numevents; j++) {
        ...
    }
    ...
    // 處理時間事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    ...
}
  • 對於文件事件,在 linux 系統中,redis 採用了 epoll 多路複用 I/O 事件驅動處理文件事件。通過 epoll_wait 撈出就緒事件進行處理。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    ...
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    ...
}
  • 先處理完文件事件,再處理時間事件。
static int processTimeEvents(aeEventLoop *eventLoop) {
    ...
}

4. 多定時任務

我們看看 processTimeEvents 是如何處理多個定時任務的:
redis 多定時任務

  • 遍歷時間事件鏈表,先刪除已處理,被標識需要刪除的事件,再執行到期事件。
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);
    ...
    eventLoop->lastTime = now;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    // 遍歷時間鏈表,處理到期執行的時間事件。
    while(te) {
        long now_sec, now_ms;
        long long id;

        // 從時間事件鏈表中,刪除時被標識爲刪除狀態的事件。
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            te = next;
            continue;
        }
        ...
        // 時間事件到期,執行事件。
        aeGetTime(&now_sec, &now_ms);
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms)) {
            int retval;

            id = te->id;
            // 執行時鐘回調函數
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            // 如果回調函數不返回 AE_NOMORE,重新更新該事件的到期時間,等待下次觸發,否則標識事件爲刪除狀態。
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}
  • 時間事件定時執行原理。

    到期事件回調處理函數 timeProc,例如 redis 對應的處理函數 serverCronserverCron 返回下一次到期的時間間隔,事件到期時間被(aeAddMillisecondsToNow)修改延後一個時間間隔,下一次到期再重新執行,從而達到時鐘定期執行的效果。

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // 返回時間間隔,單位毫秒。
    return 1000/server.hz;
}
  • 定時器定時執行頻率。

    很多後臺任務都在定時器裏執行,定時執行頻率可以由配置文件的 hz 頻率控制,時間事件 (1000/hz) 毫秒執行一次,頻率越高,到期時間間隔越小,刷得越快,定時後臺任務處理得越快,但是這樣也會相應地損耗更多的系統資源,而且定時事件和文件事件是在同一個進程中進行的,這樣肯定會影響到文件事件執行。一般情況下,系統默認一秒定時執行 10 次,也就是 hz == 10

# redis.conf

# 定時器事件刷新頻率 1 < hz < 500,默認 10
hz 10
  • 多定時任務

    事件裏有不同的定時任務,它們定時執行的任務有快有慢,那對於多個定時任務,在時間事件觸發後,它是如何處理的呢?
    可以通過宏 run_with_period 處理。當時間間隔 _ms_ 很小的時候,每次觸發時間事件,任務都會執行,否則通過記錄事件觸發的次數 server.cronloops++,當 hz 觸發的事件時間間隔累積起來達到長時間間隔,就執行慢任務。(參考上圖)

struct redisServer {
    ...
    int cronloops;              /* Number of times the cron function run */
    ...
}

#define run_with_period(_ms_) if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // 快任務,時間間隔 <= 時間事件觸發時間間隔,執行。
    run_with_period(100) {
        ...
    }
    ...
    // 慢任務,定時時間間隔比較長的,需要通過 server.cronloops 累加達到長時間間隔,纔會執行慢任務。
    run_with_period(5000) {
        ...
    }

    // 每次觸發定時事件,都會執行。
    /* We need to do a few operations on clients asynchronously. */
    clientsCron();

    /* Handle background operations on Redis databases. */
    databasesCron();
    ...
    server.cronloops++;
    ...
}

5. 總結

  • 定時器是由時間事件組成的,目前,redis 核心的定時事件只有一個,核心邏輯都在 serverCron 函數裏。
  • 定時器定時觸發頻率受配置 hz 影響,可以通過修改該配置項,調整定時處理速度。
  • 多定時任務,其實在同一個定時器時間事件裏處理。
  • 定時事件和文件事件都在同一個進程中執行,所以雖然定時事件時間精度是毫秒,但是不一定會十分精確,會受到文件事件處理影響。
  • 定時事件執行還會受到定時器裏的任務處理影響,例如系統在一個時間段內很多過期數據,那麼系統有可能會分配更多的時間片去處理。
  • 基於 redis 主邏輯都在單進程主線程中實現,所以定時任務不能執行太長的時間,所以很多複雜的定時任務,都會限制處理數量和處理時間。(例如字典 dict 的擴容和縮容,maxmemory 數據淘汰策略等等。)

7. 後記

通讀一個知識點後,知識在腦海中是模糊的,需要通過不同方式去強化清晰這個腦海中的映像。讓抽象思維落地,我自己會經常將一些知識點圖形化,這樣一點一點地將知識碎片拼接起來。
圖形化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章