【Rust】时间轮的数据结构于设计模式
概念来源
- 源自“时间轮/定时轮”(Timing Wheel) 的数据结构,最经典的是 Hashed/Hierarchical Timing Wheels(1987 年提出),此后被 Linux 内核、Netty、Kafka 等广泛采用,用于高性能定时器管理。
工作原理
- 将时间离散化为固定步长的“槽”(slot),以环形数组表示当前层的时间跨度。每次 tick,指针前进一个槽,只处理当前槽中的定时器。
- 对超出当前层跨度的定时器,放到更高层(更大步长)的槽;随着指针循环,定时器逐级“下沉”到更精细的一层,直到到期执行。
- 你仓库里的实现就是多层时间轮:L1~L5 层分别对应 100ms/1s/1min/1h/1d 步长,见 crates/focus-rs/src/slot/timer.rs:166 和 advance_time_wheel 逻辑 crates/focus-rs/src/slot/timer.rs:520。每次只清“当前槽位”的定时器,见 take_current_slot_timers crates/focus-rs/src/slot/timer.rs:199。短时(<1s)用一个小堆/有序结构处理,见 process_short_timers crates/focus-rs/src/slot/timer.rs:466。
为什么用“槽”
- 插入/删除近似 O(1):定时器加入对应槽位,tick 时只处理当前槽,避免对全量定时器做 O(n) 扫描或 O(log n) 堆操作。
- 批量处理友好:同一时间段到期的定时器集中在同一槽,tick 触发时成批拉取执行,降低锁竞争和调度开销。
- 可扩展性强:层级化后,支持从毫秒到天的宽时间范围,且每个 tick 的工作量与当前槽内的计数成正比,而不是与定时器总数成正比。
- 抖动可控:粒度由槽步长决定,可在“精度 vs 性能/内存”之间调参;你们将 <1s 交给短时队列,>=1s 用时间轮,兼顾精度与效率。
- 简化并发:槽是天然的分桶,减少全局结构上的热锁;你们代码中还将短时/长时映射分离,进一步降低争用。
相对其他方案的优势/权衡
- 相比最小堆/优先队列:插入 O(1) vs O(log n),每 tick 无需堆顶比较/调整;但时间轮存在“步长”精度限制,需要权衡 tick 和内存占用。
- 相比纯有序表:不需要每 tick 全表扫描;但取消/重置时要注意“从槽中即时移除”(我们已在 cancel/reset 中补上移除,避免槽内残留导致同 ID 冲突)。
与你们实现的映射
- 槽结构与操作:
- 层与槽:crates/focus-rs/src/slot/timer.rs:166(TimeWheelLevel)
- 取当前槽定时器:crates/focus-rs/src/slot/timer.rs:199
- 推进并级联:crates/focus-rs/src/slot/timer.rs:520
- 短时定时器(<1s):
- 处理:crates/focus-rs/src/slot/timer.rs:466
- 我们新增的即时清理(避免槽内残留):
- 从槽位移除:crates/focus-rs/src/slot/timer.rs:221
- 取消清槽:crates/focus-rs/src/slot/timer.rs:867
- 重置清槽:crates/focus-rs/src/slot/timer.rs:932
- ----------
- 层级时间轮示意(含“槽”/slot)
L4: 天(1天/槽, 30槽)
[ 指针→ ] [ 0 ][ 1 ][ 2 ] ... [ 29 ]
↑ T4=3天 放在 L4 +3 槽
L3: 小时(1小时/槽, 24槽)
[ 指针→ ] [ 0 ][ 1 ][ 2 ] ... [ 23 ]
↑ T3=5小时 放在 L3 +5 槽
L2: 分钟(1分钟/槽, 60槽)
[ 指针→ ] [ 0 ][ 1 ][ 2 ] ... [ 59 ]
↑ T2=3分钟 放在 L2 +3 槽
L1: 秒(1秒/槽, 60槽)
[ 指针→ ] [ 0 ][ 1 ][ 2 ] ... [ 59 ]
↑ T1=7秒 放在 L1 +7 槽
L0: 毫秒(100ms/槽, 10槽)
[ 指针→ ] [ 0 ][ 1 ] ... [ 9 ]
(<1s 的短时定时器不入轮,走短时队列)
运行流程(简化)
- 新定时器加入时,根据剩余时间选择层级和槽位:
- <1s → 短时队列(按到期时间排序,独立处理)
- ≤60s → L1(秒级槽)
- ≤60min → L2(分钟级槽)
- ≤24h → L3(小时级槽)
- ≤30d → L4(天级槽)
- 每个 tick:
- 处理短时队列中到期项
- 低层(L0→L4)按顺序推进当前槽位指针,仅取“当前槽”的定时器触发
- 若某层转了一圈(指针回到0),就把上层(更大粒度)的定时器根据精确剩余时间“下沉”到本层合适的槽(级联/下沉),等待后续更精细的触发
- 新定时器加入时,根据剩余时间选择层级和槽位:
示例(与你日志一致)
- 6.94s → L1 +7 槽;70s → L2 +2 槽(先挂分钟级,等 L1 多圈后再下沉)
- 339ms → 短时队列(不入轮),到时直接触发
- 触发闭环:触发 → 激活下一段 → 立刻按剩余时长重新入轮(或入短时队列)
直观好处
- 每次 tick 只处理“当前槽”,近似 O(1),不用全量扫描或堆调整
- 到期集中批处理,锁竞争小,可扩展到大量定时器
- 多层粒度兼顾“精度与性能”:短时精确,长时高效
