时间事件(Time Event)源码详解

一、概述

Redis 的事件驱动机制中,**时间事件(Time Event)**是与文件事件并列的另一类核心事件。时间事件用于处理定时任务,如服务器定时维护、客户端超时检测、AOF 持久化、主从复制心跳等。Redis 通过时间事件实现了一个轻量级的定时器机制,在单线程中高效地管理各类周期性任务。

本文将深入剖析 Redis 时间事件的实现原理,从数据结构到核心 API,再到实际应用场景。

二、核心数据结构

2.1 时间事件结构体 aeTimeEvent

 1// src/ae.h:78-88
 2typedef struct aeTimeEvent {
 3    long long id;              // 时间事件唯一标识符,全局递增
 4    long when_sec;             // 触发时间的秒数部分(绝对时间)
 5    long when_ms;              // 触发时间的毫秒数部分(绝对时间)
 6    aeTimeProc *timeProc;      // 时间事件回调函数
 7    aeEventFinalizerProc *finalizerProc;  // 事件销毁时的清理函数(可选)
 8    void *clientData;          // 用户自定义数据,传递给回调函数
 9    struct aeTimeEvent *next;  // 指向下一个时间事件,形成单向链表
10} aeTimeEvent;

字段解析

  • id:时间事件的唯一标识符,全局递增,用于删除特定事件
  • when_secwhen_ms:事件触发的绝对时间(秒 + 毫秒)
  • timeProc:事件触发时的回调函数,返回值决定事件是否继续周期执行
  • finalizerProc:事件被删除时的清理回调(可选)
  • clientData:传递给回调函数的用户数据
  • next:指向下一个时间事件节点,时间事件以无序链表组织

2.2 事件循环中的时间事件

 1// src/ae.h:97-109
 2typedef struct aeEventLoop {
 3    int maxfd;   // 当前注册的最大文件描述符
 4    int setsize; // 事件表容量上限
 5    long long timeEventNextId;   // 下一个时间事件的 ID(全局递增计数器)
 6    time_t lastTime;             // 上次处理时间事件的时间,用于检测时钟偏移
 7    aeFileEvent *events;         // 注册的文件事件表
 8    aeFiredEvent *fired;         // 已触发的文件事件表
 9    aeTimeEvent *timeEventHead;  // 时间事件链表头节点
10    int stop;                    // 停止标志
11    void *apidata;               // 多路复用 API 的私有数据
12    aeBeforeSleepProc *beforesleep;  // 事件循环前回调
13    aeBeforeSleepProc *aftersleep;   // 事件循环后回调
14} aeEventLoop;

关键点

  • timeEventNextId:全局递增的事件 ID 计数器
  • timeEventHead:时间事件链表的头节点,所有时间事件通过链表串联
  • lastTime:记录上次处理时间事件的时间,用于检测系统时钟偏移

三、时间事件的类型

Redis 的时间事件主要分为两类:

3.1 一次性时间事件

事件触发执行后自动删除,不再重复执行。通过回调函数返回 AE_NOMORE 实现:

1// src/ae.h:51
2#define AE_NOMORE -1

3.2 周期性时间事件

事件触发执行后,根据回调函数的返回值重新计算下次触发时间,实现周期执行。返回值为距离下次触发的毫秒数。

Redis 中最典型的周期性时间事件是 serverCron,默认每 100ms 执行一次。

四、核心 API 实现

4.1 创建时间事件 aeCreateTimeEvent

 1// src/ae.c:178-203
 2long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
 3        aeTimeProc *proc, void *clientData,
 4        aeEventFinalizerProc *finalizerProc)
 5{
 6    long long id = eventLoop->timeEventNextId++;  // 分配全局唯一 ID
 7    aeTimeEvent *te;
 8
 9    te = zmalloc(sizeof(*te));  // 分配时间事件结构体内存
10    if (te == NULL) return AE_ERR;
11    te->id = id;
12    // 计算触发的绝对时间:当前时间 + 延迟毫秒数
13    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
14    te->timeProc = proc;              // 设置时间事件回调函数
15    te->finalizerProc = finalizerProc;  // 设置销毁时的清理函数
16    te->clientData = clientData;      // 设置用户自定义数据
17    // 头插法插入链表:新事件插入到链表头部
18    te->next = eventLoop->timeEventHead;
19    eventLoop->timeEventHead = te;
20    return id;  // 返回事件 ID,用于后续删除操作
21}

执行流程

  1. 分配全局唯一的事件 ID
  2. 分配并初始化时间事件结构体
  3. 计算事件触发的绝对时间
  4. 将事件插入链表头部
  5. 返回事件 ID

4.2 删除时间事件 aeDeleteTimeEvent

 1// src/ae.c:205-232
 2int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
 3{
 4    aeTimeEvent *te = eventLoop->timeEventHead;  // 从链表头开始遍历
 5    aeTimeEvent *prev = NULL;  // 记录前驱节点,用于删除操作
 6
 7    // 遍历链表查找指定 ID 的时间事件
 8    while(te) {
 9        if (te->id == id) {
10            // 找到目标事件,执行删除
11            if (prev == NULL)
12                // 删除的是头节点,更新头指针指向下一个节点
13                eventLoop->timeEventHead = te->next;
14            else
15                // 删除中间或尾部节点,调整前驱的 next 指针
16                prev->next = te->next;
17            // 如果设置了清理函数,则调用它释放相关资源
18            if (te->finalizerProc)
19                te->finalizerProc(eventLoop, te->clientData);
20            zfree(te);  // 释放时间事件结构体内存
21            return AE_OK;
22        }
23        prev = te;   // 记录当前节点作为前驱
24        te = te->next;  // 移动到下一个节点
25    }
26    return AE_ERR; /* NO event with the specified ID found */
27}

执行流程

  1. 遍历时间事件链表
  2. 根据事件 ID 查找目标事件
  3. 从链表中移除该事件
  4. 调用清理函数(如有)
  5. 释放内存

4.3 查找最近的时间事件 aeSearchNearestTimer

 1// src/ae.c:234-252
 2static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
 3{
 4    aeTimeEvent *te = eventLoop->timeEventHead;  // 从链表头开始遍历
 5    aeTimeEvent *nearest = NULL;  // 记录最近的时间事件
 6
 7    // 遍历整个链表,找到触发时间最早的事件
 8    while(te) {
 9        // 比较 when_sec 和 when_ms,找到最小的时间
10        if (!nearest || te->when_sec < nearest->when_sec ||
11                (te->when_sec == nearest->when_sec &&
12                 te->when_ms < nearest->when_ms))
13            nearest = te;  // 更新最近事件
14        te = te->next;
15    }
16    return nearest;  // 返回最近的时间事件,可能为 NULL
17}

说明

  • 时间事件链表是无序的,需要遍历查找最早触发的事件
  • 该函数用于计算 aeApiPoll 的超时时间,确保不会错过时间事件
  • 时间复杂度 O(n),但 Redis 通常只有 1 个时间事件,开销可忽略

4.4 处理时间事件 processTimeEvents

 1// src/ae.c:254-336
 2static int processTimeEvents(aeEventLoop *eventLoop) {
 3    int processed = 0;  // 统计已处理的事件数量
 4    aeTimeEvent *te;
 5    long long maxId;
 6    time_t now = time(NULL);  // 获取当前时间(秒)
 7
 8    // 检测系统时钟是否被向后调整(时钟回退)
 9    /* If the system clock is moved to the future, and then set back to the
10     * right value, time events may be delayed in a random way. Often this
11     * means that scheduled operations will not be performed fast enough.
12     *
13     * Here we try to detect system clock skews, and force a timeout of all
14     * the time events that are to be processed in the next 1 second, so
15     * that such events will be processed ASAP. */
16    if (now < eventLoop->lastTime) {
17        // 时钟被向后调整,立即触发所有时间事件
18        // 将所有事件的触发时间设为 0,使其立即执行
19        te = eventLoop->timeEventHead;
20        while(te) {
21            te->when_sec = 0;  // 设置为立即触发
22            te = te->next;
23        }
24    }
25    eventLoop->lastTime = now;  // 更新上次处理时间
26
27    // 遍历处理所有到期的时间事件
28    te = eventLoop->timeEventHead;
29    maxId = eventLoop->timeEventNextId-1;  // 记录当前最大 ID
30    while(te) {
31        long long id;
32
33        // 跳过在本次处理过程中新创建的事件(ID > maxId)
34        // 防止新事件在本次循环中被立即处理
35        if (te->id > maxId) {
36            te = te->next;
37            continue;
38        }
39
40        // 获取当前精确时间(秒 + 毫秒)
41        // aeGetTime(&now, &now_ms);  // 实际代码中有这行
42        // 检查事件是否已到期:触发时间 <= 当前时间
43        if (te->when_sec < now ||
44            (te->when_sec == now && te->when_ms <= now_ms))
45        {
46            int retval;
47            id = te->id;
48            // 调用时间事件回调函数
49            retval = te->timeProc(eventLoop, id, te->clientData);
50            processed++;  // 统计已处理的事件数
51
52            // 根据返回值决定事件是否继续
53            if (retval != AE_NOMORE) {
54                // 周期事件:重新计算下次触发时间
55                // retval 为距离下次触发的毫秒数
56                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
57            } else {
58                // 一次性事件:返回 AE_NOMORE,删除事件
59                aeDeleteTimeEvent(eventLoop, id);
60            }
61            // 事件链表可能已被修改(如删除了事件),重新从头遍历
62            te = eventLoop->timeEventHead;
63        } else {
64            // 事件未到期,继续检查下一个
65            te = te->next;
66        }
67    }
68    return processed;  // 返回已处理的事件数量
69}

执行流程

  1. 检测系统时钟偏移,处理时钟回退情况
  2. 遍历时间事件链表
  3. 检查每个事件是否已到期
  4. 到期事件:调用回调函数,根据返回值决定是否继续
  5. 返回处理的事件数量

4.5 计算超时时间

aeProcessEvents 中,会根据最近的时间事件计算 aeApiPoll 的超时时间:

 1// src/ae.c:365-407
 2    // 判断是否需要计算超时时间
 3    if (eventLoop->maxfd != -1 ||
 4        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
 5        int j;
 6        aeTimeEvent *shortest = NULL;  // 最近的时间事件
 7        struct timeval tv, *tvp;
 8
 9        // 查找最近的时间事件
10        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
11            shortest = aeSearchNearestTimer(eventLoop);
12        if (shortest) {
13            // 存在时间事件,计算超时时间
14            long now_sec, now_ms;
15
16            aeGetTime(&now_sec, &now_ms);  // 获取当前时间
17            tvp = &tv;
18
19            /* How many milliseconds we need to wait for the next
20             * time event to fire? */
21            // 计算距离最近时间事件还有多少毫秒
22            long long ms =
23                (shortest->when_sec - now_sec)*1000 +
24                shortest->when_ms - now_ms;
25
26            if (ms > 0) {
27                // 还有剩余时间,设置为超时值
28                tvp->tv_sec = ms/1000;          // 秒部分
29                tvp->tv_usec = (ms % 1000)*1000; // 微秒部分
30            } else {
31                // 时间已到,立即返回不阻塞
32                tvp->tv_sec = 0;
33                tvp->tv_usec = 0;
34            }
35        } else {
36            /* If we have to check for events but need to return
37             * ASAP because of AE_DONT_WAIT we need to set the timeout
38             * to zero */
39            // 没有时间事件
40            if (flags & AE_DONT_WAIT) {
41                // 设置了不等待标志,立即返回
42                tv.tv_sec = tv.tv_usec = 0;
43                tvp = &tv;
44            } else {
45                /* Otherwise we can block */
46                // 没有时间事件且允许等待,无限阻塞直到有文件事件
47                tvp = NULL; /* wait forever */
48            }
49        }
50
51        /* Call the multiplexing API, will return only on timeout or when
52         * some event fires. */
53        // 调用 I/O 多路复用,等待文件事件或超时
54        numevents = aeApiPoll(eventLoop, tvp);

说明

  • 如果有时间事件,计算最近事件的剩余时间作为超时值
  • 如果没有时间事件,可能无限等待(直到有文件事件)
  • 如果设置了 AE_DONT_WAIT,超时为 0,立即返回

五、时间事件在 Redis 中的应用

5.1 serverCron 的注册

Redis 启动时,会在 initServer 中注册 serverCron 时间事件:

 1// src/server.c:2115-2117
 2    /* Create the timer callback, this is our way to process many background
 3     * operations incrementally, like clients timeout, eviction of unaccessed
 4     * expired keys and so forth. */
 5    // 创建定时器回调,这是处理后台任务的核心机制
 6    // 包括客户端超时、过期键清理等
 7    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
 8        serverPanic("Can't create event loop timers.");
 9        exit(1);
10    }

serverCron 是 Redis 的核心定时任务,负责:

  • 更新服务器时间缓存
  • 处理客户端超时
  • 执行过期键清理
  • 触发 AOF 重写/RDB 保存
  • 主从复制心跳
  • 集群心跳
  • 等等…

5.2 serverCron 的实现

  1// src/server.c:1090-1352
  2int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
  3    int j;
  4    UNUSED(eventLoop);
  5    UNUSED(id);
  6    UNUSED(clientData);
  7
  8    /* Software watchdog: deliver the SIGALRM that will reach the signal
  9     * handler if we don't return here fast enough. */
 10    // 软件看门狗:如果 serverCron 执行超时,发送 SIGALRM 信号
 11    if (server.watchdog_period > 0) watchdogScheduleSignal(server.watchdog_period);
 12
 13    /* Update the time cache. */
 14    updateCachedTime(1);  // 更新服务器时间缓存,避免频繁调用 time()
 15    server.lruclock = getLRUClock();  // 更新 LRU 时钟,用于键淘汰
 16
 17    /* Record the max memory used since the server was started. */
 18    // 记录峰值内存使用量
 19    if (zmalloc_used_memory() > server.stat_peak_memory)
 20        server.stat_peak_memory = zmalloc_used_memory();
 21
 22    /* Sample the RSS here since this is a relatively slow call. */
 23    server.resident_set_size = zmalloc_get_rss();  // 获取进程实际物理内存
 24
 25    /* We received a SIGTERM, shutting down here in a safe way, as it is
 26     * ok not to do anything but exit in an asyncronous event handler. */
 27    // 处理 SIGTERM 信号,安全关闭服务器
 28    if (server.shutdown_asap) {
 29        if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
 30        serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
 31        server.shutdown_asap = 0;
 32    }
 33
 34    /* Show some info about non-empty databases */
 35    // 每 5 秒打印一次数据库统计信息
 36    run_with_period(5000) {
 37        for (j = 0; j < server.dbnum; j++) {
 38            long long size, used, vkeys;
 39
 40            size = dictSlots(server.db[j].dict);      // 哈希表槽位数
 41            used = dictSize(server.db[j].dict);       // 已使用键数量
 42            vkeys = dictSize(server.db[j].expires);   // 设置过期时间的键数量
 43            if (used || vkeys) {
 44                serverLog(LL_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lld slots HT.",j,used,vkeys,size);
 45            }
 46        }
 47    }
 48
 49    /* Show information about connected clients */
 50    // 每 5 秒打印一次客户端连接信息
 51    if (!server.sentinel_mode) {
 52        run_with_period(5000) {
 53            serverLog(LL_VERBOSE,
 54                "%lu clients connected (%lu replicas), %zu bytes in use",
 55                listLength(server.clients)-listLength(server.slaves),
 56                listLength(server.slaves),
 57                zmalloc_used_memory());
 58        }
 59    }
 60
 61    /* We need to do a few operations on clients asynchronously. */
 62    clientsCron();  // 异步处理客户端相关任务:超时检测、输入输出缓冲区检查
 63
 64    /* Handle background operations on Redis databases. */
 65    databasesCron();  // 数据库后台任务:过期键清理、字典 rehash
 66
 67    /* Start a scheduled AOF rewrite if this was requested by the user while
 68     * a BGSAVE was in progress. */
 69    // 如果 AOF 重写被延迟(因为当时有 BGSAVE),现在尝试执行
 70    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
 71        server.aof_rewrite_scheduled)
 72    {
 73        rewriteAppendOnlyFileBackground();
 74    }
 75
 76    /* Check if a background saving or rewrite in progress terminated. */
 77    // 检查后台子进程是否结束(RDB 或 AOF)
 78    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
 79        ldbPendingChildren())
 80    {
 81        int statloc;
 82        pid_t pid;
 83
 84        // 非阻塞等待子进程结束
 85        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
 86            int exitcode = WEXITSTATUS(statloc);  // 退出码
 87            int bysignal = 0;
 88
 89            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);  // 终止信号
 90
 91            if (pid == -1) {
 92                // wait3 出错
 93                serverLog(LL_WARNING,"wait3() returned an error: %s. "
 94                    "rdb_child_pid = %d, aof_child_pid = %d",
 95                    strerror(errno),
 96                    (int) server.rdb_child_pid,
 97                    (int) server.aof_child_pid);
 98            } else if (pid == server.rdb_child_pid) {
 99                // RDB 后台保存完成
100                backgroundSaveDoneHandler(exitcode,bysignal);
101            } else if (pid == server.aof_child_pid) {
102                // AOF 后台重写完成
103                backgroundRewriteDoneHandler(exitcode,bysignal);
104            } else {
105                if (!ldbPendingChildren()) {
106                    serverLog(LL_WARNING,
107                        "Warning, detected child with unmatched pid: %ld",
108                        (long)pid);
109                }
110            }
111            updateDictResizePolicy();  // 根据是否有子进程调整字典 rehash 策略
112        }
113    } else {
114        /* If there is not a background saving/rewrite in progress check if
115         * we have to save/rewrite now */
116        // 没有后台任务,检查是否需要执行 RDB 保存
117        for (j = 0; j < server.saveparamslen; j++) {
118            struct saveparam *sp = server.saveparams+j;
119
120            /* saved < changes && elapsed >= seconds */
121            // 检查 save 配置条件:变更数 >= sp->changes 且时间 >= sp->seconds
122            if (server.dirty >= sp->changes &&
123                server.unixtime-server.lastsave > sp->seconds) {
124                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
125                    sp->changes, (int)sp->seconds);
126                rdbSaveInfo rsi, *rsiptr;
127                rsiptr = rdbPopulateSaveInfo(&rsi);
128                rdbSaveBackground(server.rdb_filename,rsiptr);
129                break;
130            }
131        }
132
133        /* Trigger an AOF rewrite if needed */
134        // 检查是否需要触发 AOF 重写
135        if (server.rdb_child_pid == -1 &&
136            server.aof_child_pid == -1 &&
137            server.aof_rewrite_perc &&
138            server.aof_current_size > server.aof_rewrite_min_size)
139        {
140            // AOF 文件增长率超过阈值时触发重写
141            long long baseline = server.aof_rewrite_base_size ?
142                server.aof_rewrite_base_size : 1;
143            long long growth = (server.aof_current_size*100/baseline) - 100;
144            if (growth >= server.aof_rewrite_perc) {
145                serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
146                rewriteAppendOnlyFileBackground();
147            }
148        }
149    }
150    // ... 更多任务
151}

5.3 serverCron 的返回值

serverCron 返回下次执行的间隔时间(毫秒):

1// src/server.c:1351-1352
2    server.cronloops++;  // 增加循环计数器,用于 run_with_period 宏
3    return 1000/server.hz;  // 返回下次执行的毫秒数

说明

  • server.hz 默认值为 10,表示每秒执行 10 次
  • 返回 1000/10 = 100,即每 100ms 执行一次
  • 可通过配置文件调整 hz 参数,影响定时任务的执行频率

5.4 run_with_period 宏

serverCron 中大量使用 run_with_period 宏来控制任务执行频率:

1// src/server.h:1397-1401
2#define run_with_period(_ms_) if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))

宏解析

  • 如果 _ms_ <= 1000/server.hz,则每次 serverCron 都执行
  • 否则,每隔 _ms_/(1000/server.hz) 次循环执行一次

用法示例

1// 每 5 秒打印一次数据库信息
2run_with_period(5000) {
3    // 打印数据库统计信息
4}
5
6// 每 100ms 执行一次集群心跳
7run_with_period(100) {
8    if (server.cluster_enabled) clusterCron();
9}

六、时间事件处理流程图

 1                    ┌─────────────────────────────────────────┐
 2                    │              aeMain()                   │
 3                    │           while(!stop)                  │
 4                    └─────────────────┬───────────────────────┘
 5 6 7                    ┌─────────────────────────────────────────┐
 8                    │          beforesleep()                  │
 9                    │   (处理待发送响应、快速过期检查等)       │
10                    └─────────────────┬───────────────────────┘
111213                    ┌─────────────────────────────────────────┐
14                    │         aeProcessEvents()               │
15                    └─────────────────┬───────────────────────┘
161718                    ┌─────────────────────────────────────────┐
19                    │    aeSearchNearestTimer()               │
20                    │    查找最近的时间事件                   │
21                    └─────────────────┬───────────────────────┘
222324                    ┌─────────────────────────────────────────┐
25                    │    计算超时时间(距离最近事件的毫秒数)       │
26                    │    tvp = nearest_when - now             │
27                    └─────────────────┬───────────────────────┘
282930                    ┌─────────────────────────────────────────┐
31                    │         aeApiPoll(tvp)                  │
32                    │    等待文件事件或超时                      │
33                    └─────────────────┬───────────────────────┘
3435                    ┌─────────────────┴───────────────────────┐
36                    │                                       │
37                    ▼                                       ▼
38          ┌─────────────────┐                   ┌─────────────────┐
39          │  文件事件触发     │                   │   超时返回        │
40          │  (处理文件事件)   │                   │ (无文件事件)      │
41          └────────┬────────┘                   └────────┬────────┘
42                   │                                     │
43                   └─────────────────┬───────────────────┘
444546                    ┌─────────────────────────────────────────┐
47                    │        processTimeEvents()              │
48                    │        处理到期的时间事件               │
49                    └─────────────────┬───────────────────────┘
505152                    ┌─────────────────────────────────────────┐
53                    │           serverCron()                  │
54                    │  ┌─────────────────────────────────┐   │
55                    │  │ - 更新时间缓存                   │   │
56                    │  │ - 清理超时客户端                 │   │
57                    │  │ - 过期键删除                     │   │
58                    │  │ - 触发 AOF/RDB 后台任务         │   │
59                    │  │ - 主从复制心跳                   │   │
60                    │  │ - 集群心跳                       │   │
61                    │  │ - ...                            │   │
62                    │  └─────────────────────────────────┘   │
63                    │         return 1000/server.hz          │
64                    └─────────────────────────────────────────┘

七、设计亮点

7.1 无序链表 + 遍历查找

时间事件采用无序链表组织,查找最近事件需要遍历整个链表。

为什么不用更高效的数据结构(如最小堆、时间轮)?

  1. 时间事件数量极少:Redis 通常只有 serverCron 一个时间事件
  2. 实现简单:链表插入 O(1),删除 O(n),总体开销可忽略
  3. 内存紧凑:无需额外的数据结构开销

7.2 时钟偏移检测

Redis 会检测系统时钟是否被向后调整:

 1// src/ae.c:268-281
 2    // 如果当前时间 < 上次处理时间,说明时钟被回退
 3    if (now < eventLoop->lastTime) {
 4        te = eventLoop->timeEventHead;
 5        while(te) {
 6            te->when_sec = 0;  // 设置为 0,立即触发
 7            te = te->next;
 8        }
 9    }
10    eventLoop->lastTime = now;

说明:如果检测到时钟回退,立即触发所有时间事件,确保定时任务不会被延迟过久。

7.3 周期事件的处理

通过回调函数返回值实现周期执行:

  • 返回 AE_NOMORE (-1):一次性事件,执行后删除
  • 返回正整数:周期事件,返回值为下次执行的延迟毫秒数

这种设计避免了为周期事件创建新对象,复用同一个事件结构体。

7.4 与文件事件的协同

时间事件与文件事件在同一个事件循环中处理:

  • 先处理文件事件
  • 再处理时间事件
  • 时间事件影响 aeApiPoll 的超时时间

这确保了时间事件不会无限延迟,同时也不会阻塞文件事件的处理。

八、时间事件 vs 文件事件

特性 时间事件 文件事件
触发条件 到达指定时间点 fd 可读/可写
数据结构 无序链表 fd 索引数组
处理顺序 后处理 先处理
典型用途 定时任务 网络 I/O
典型回调 serverCron readQueryFromClient
数量 通常 1 个 随连接数变化

九、总结

Redis 时间事件机制的核心设计要点:

特性 说明
轻量级实现 无序链表,O(n) 查找,适配少量时间事件
周期复用 通过返回值控制周期执行,避免重复创建
时钟容错 检测时钟回退,确保任务及时执行
协同调度 与文件事件协同,计算合理超时时间
统一抽象 通过 aeTimeEvent 统一管理定时任务

时间事件是 Redis 实现定时任务的核心机制,serverCron 作为唯一的时间事件,承载了服务器的大部分后台维护工作。理解时间事件的实现原理,有助于深入掌握 Redis 的内部运行机制。

— END —