edis 的时间轮算法是其实现定时任务调度和键过期处理的核心机制之一。尽管 Redis 官方并未直接公开其时间轮的具体实现细节,但结合其设计思想和相关技术文档,可以总结出以下关键原理和应用场景:
一、时间轮算法的基本原理
时间轮是一种高效的定时任务调度数据结构,其核心思想是将时间划分为环形槽位(slot),每个槽位对应固定的时间间隔。指针按固定周期移动,触发槽位中的任务执行。
- 核心组成:
- 环形槽位数组:每个槽位存储待执行的任务链表。
- 指针:按固定时间间隔(如 100ms)移动,触发当前槽位任务。
- 任务链表:按触发时间排序的待处理任务。
- 工作流程:
- 添加任务时,根据延迟时间计算目标槽位位置。
- 指针周期性移动,执行当前槽位任务,并将未到期的任务重新分配到后续槽位
时间轮算法的本质是将时间离散化为环形结构的时间槽,通过指针转动触发对应槽内的任务。但 Redis 并未直接实现标准时间轮,而是采用 **「最小堆 + 事件循环 + 时间轮逻辑」的混合方案 **,其核心设计原因在于:
- 单线程模型下需高效处理海量定时任务(如过期键、持久化、心跳);
- 避免传统链表遍历的 O (n) 复杂度,同时兼容短周期与长周期任务。
Redis 将每个定时任务封装为aeTimeEvent
结构体,定义于ae.h
:
typedef struct aeTimeEvent {
long long id; // 事件唯一ID,自增生成
long when_sec; // 触发时间的秒数(Unix时间戳)
long when_ms; // 触发时间的毫秒数
aeTimeEventProc *proc; // 事件处理函数指针
struct aeTimeEvent *next; // 链表指针(用于旧版本兼容)
} aeTimeEvent;
关键点:
when_sec
和when_ms
组合表示绝对触发时间,而非相对延迟;
- 早期版本通过链表管理事件,但 O (n) 遍历效率低,Redis 2.4 后引入最小堆优化。
Redis 使用aeEventLoop
结构体中的timeEventHead
指针指向最小堆顶,堆节点为aeTimeEvent
:
- 堆中节点按
when_sec
和when_ms
排序,堆顶始终是最早触发的事件;
- 插入新事件时通过
aeAddMillisecondsToNow
计算触发时间,并执行堆调整(时间复杂度 O (logn));
- 事件循环每次仅需检查堆顶事件是否到期(O (1) 复杂度)。
Redis 主循环aeMain
中,aeProcessEvents
函数按以下逻辑执行:Redis 的时间轮由事件循环(Event Loop)驱动,与 I/O 多路复用(如 epoll)共享同一线程,避免线程切换开销。
- 计算堆顶事件的剩余时间
timeout
;
- 使用
epoll/poll
阻塞等待timeout
毫秒,同时处理文件事件;
- 若堆顶事件到期,执行其处理函数,并根据返回值决定是否重新入堆。
- 动态槽间隔:通过调整
tick_duration
和 slots
数量平衡精度与内存占用。例如,毫秒级任务需缩小 tick_duration
,但会增加槽数量。
- 多层时间轮:对超长延迟任务(如数小时后执行),采用分层时间轮(Hierarchical Time Wheels),将任务分配到不同层级的时间轮中。
Redis 通过时间轮(小根堆+单线程事件循环 I/O 多路复用)驱动activeExpireCycle
函数,每秒执行 10 次(默认hz=10
):
- 随机选择
DB
,检查桶中键的过期时间;
- 每次最多删除
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
个键(默认 20),避免阻塞;
- 类似时间轮 “指针转动”,每个时间槽对应一次检查周期,过期键按哈希桶分布到不同 “槽” 中。
根据redis.conf
中的save
配置(如save 900 1
):
- 时间轮定期检查数据修改次数与时间间隔;
- 满足条件时触发 RDB 快照或 AOF 重写,例如每 900 秒至少 1 次写操作。
在哨兵模式和集群模式中:
- 时间轮控制心跳包发送频率(默认每秒 1 次);
- 检测节点超时(如
down-after-milliseconds
配置),触发故障转移。
维度 | 标准时间轮(如 HashedWheelTimer) | Redis 时间轮实现 |
数据结构 |
单层 / 多层环形数组 |
最小堆 + 事件链表 |
时间精度 |
固定时间槽(如 10ms / 槽) |
毫秒级精度(依赖系统调用) |
长周期任务处理 |
多层时间轮跳转(如秒→分→时轮) |
最小堆直接存储绝对时间 |
周期性任务支持 |
天然支持(槽复用) |
通过事件处理函数返回新时间实现 |
线程模型 |
多线程并发处理 |
单线程事件循环 |
Redis 的优化逻辑:
- 单线程场景下,最小堆的 O (logn) 操作比多层时间轮的状态维护更轻量;
- 时间轮逻辑与事件循环深度耦合,避免额外的数据结构开销。
- 过期键检查、AOF 重写等耗时任务采用分片处理:
// activeExpireCycle源码片段
for (i = 0; i < dbs_per_call; i++) {
db = server.db + (current_db % server.dbnum);
current_db++;
// 处理当前DB的过期键,最多处理LOOKUPS_PER_LOOP个
}
- 每次仅处理部分任务,防止单次操作阻塞主线程。
通过hz
配置(1-500)控制时间事件检查频率:
hz=10
(默认):适用于一般场景,CPU 占用低;
hz=100
:高负载时提高过期检查精度,代价是 CPU 占用上升约 10%。
- 对大键删除等耗时操作,时间轮触发
lazyfree
机制,将任务放入后台线程:
// unlink命令的处理逻辑
if (server.lazyfree_lazy_unlink) {
bioCreateJob(BIO_LAZY_FREE, key); // 放入后台线程池
} else {
dbDelete(db, key); // 同步删除
}
以过期键检查为例,时间轮的触发路径如下:
- 事件注册:
aeMain
初始化时,通过aeCreateTimeEvent
注册过期检查事件,触发时间为now + 100ms
(假设hz=10
);
- 事件循环:
aeProcessEvents
计算堆顶事件剩余时间,调用epoll_wait
阻塞;
- 时间轮 “转动”:100ms 后事件到期,执行
serverCron
函数(时间事件处理函数);
- 任务处理:
serverCron
中调用activeExpireCycle
,按分片策略检查过期键;
- 事件重置:处理完成后,重新计算下一次触发时间(
now + 100ms
),事件重新入堆。
-
根据业务场景调整 hz 参数:
- 缓存场景(过期键少):保持
hz=10
;
- 实时数据场景(过期键多):设为
hz=50-100
,但需监控 CPU 使用率。
-
避免大键集中过期:
- 通过
TOMBSTONE
机制减少过期键检查压力;
- 使用
redis-cli --bigkeys
提前发现大键,分散过期时间。
-
监控时间事件耗时:
- 通过
INFO server
查看latest_fork_usec
(RDB 耗时)、expired_keys
(每秒过期键数);
- 若
cpu_user
持续高于 20%,可能是时间轮任务过重,需优化过期策略。
Redis 的时间轮实现并非追求理论上的完美算法,而是在单线程模型下做了以下平衡:
- 效率与简单性:用最小堆 + 事件循环替代复杂的多层时间轮,降低代码维护成本;
- 实时性与资源消耗:通过可配置的
hz
参数,让用户在 CPU 占用与过期精度间取舍;
- 阻塞避免:渐进式处理和后台线程机制,确保时间轮任务不会阻塞主线程。
理解这一设计思想,不仅能深入掌握 Redis 的定时任务机制,也为分布式系统中的时间调度提供了工程实践参考。