定時器是 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. 單進程異步處理事件邏輯
進程通過循環,不停地處理時間事件和文件事件。
- 在進程的循環中不停地撈出文件和時間事件進行處理
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
是如何處理多個定時任務的:
- 遍歷時間事件鏈表,先刪除已處理,被標識需要刪除的事件,再執行到期事件。
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 對應的處理函數serverCron
。serverCron
返回下一次到期的時間間隔,事件到期時間被(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. 後記
通讀一個知識點後,知識在腦海中是模糊的,需要通過不同方式去強化清晰這個腦海中的映像。讓抽象思維落地,我自己會經常將一些知識點圖形化,這樣一點一點地將知識碎片拼接起來。