固定数组时间轮的槽过载优化:桶链表与批次执行

固定数组时间轮的槽过载优化:桶链表与批次执行

时间轮是处理海量定时任务的标准结构,它把时间切分成固定长度的槽,每个槽挂一个定时器链表。tick 推进时扫描当前槽链表,触发所有到期任务。理想情况下,每次 tick 的复杂度是 O(K),K 为该槽上的到期定时器数,通常是常数。但当瞬时大量任务堆积在同一个槽时------比如 1ms 精度的轮子,某个时刻同时到了上万个超时------遍历链表会拖慢整个 tick,后续槽的处理被推迟,定时精度整体恶化。

这篇笔记记录一种简单直接的工程优化:让一个槽可以挂多个子桶(桶链表),并限制每次 tick 触发的任务数量,用可接受的延迟抖动换取处理时间的上界。


1. 基础结构:数组 + 双向链表

先看最简模型。一个时间轮就是一个定长数组 slots[N],每个元素是双向链表的头指针。定时器节点内嵌 prev/next 指针,插入时计算到期槽位 (cur + delay_ticks) % N 并加入链表头,tick 时遍历该槽链表、触发回调并摘除。数组大小固定,没有动态分配。

当槽内定时器很少时,这完全够用。


2. 槽过载:一个桶装不下

当某个槽的链表过长,单次 tick 遍历成本可能超过一个槽的时长(比如 1ms),形成雪崩。直观解法是横向扩展------一个槽不再只是一个链表,而是一串"小桶",每个小桶内部又是一个定时器链表。

复制代码
原结构: slot[i] -> Node -> Node -> ... (单链表)
新结构: slot[i] -> Bucket1 -> Bucket2 -> ...
                    |            |
                    v            v
                  Node链表     Node链表

桶单元(Bucket) 是一个固定或动态分配的小结构:

  • 内部包含一个定时器双向链表的头指针(和尾指针,便于尾部插入)。
  • 一个 next 指针,指向同槽的下一个桶单元。
  • 可选:当前桶内的定时器数量,用于快速判断是否已满。

这样就把一个长链表切分成了多个短链表,每个桶单元内部长度可控(比如限制最多 128 个节点)。当某个桶满了,就从预分配的对象池里取一个空桶,挂到桶链尾部。


3. 插入定时器:选定桶并挂载

插入流程不变:先根据延迟计算出目标槽索引 idx。然后找到该槽的桶链,一般是追加到最后一个桶单元(这样可以保证先入先出)。如果最后一个桶已满,就新建一个桶单元接在后面,再插入。

伪逻辑:

复制代码
bucket = slots[idx].tail_bucket;
if bucket is full:
    bucket = alloc_bucket();
    slots[idx].tail_bucket.next = bucket;
    slots[idx].tail_bucket = bucket;
insert timer node into bucket.timer_list;

这里完全保持了 O(1) 插入。


4. 批次执行:每次 tick 只触发固定数量

为了避免一次 tick 触发整个槽的几百上千个任务,我们引入一个批次上限 BATCH_SIZE(例如 128)。每次 tick 处理当前槽时,只触发最多 BATCH_SIZE 个任务,然后停止。未处理完的任务继续留在桶内,等待下次 tick 再处理。

这会使部分任务的真实触发时间延后几个 tick,但只要批次上限设置合理(比如最大延迟 = BATCH_SIZE × tick_interval),这个额外延迟是可量化的、受控的。对于大多数超时场景(如 30s 连接超时,1ms tick),延时几个毫秒完全可以接受。

处理流程(每个 tick):

  1. 取当前槽索引 cur = (cur + 1) % N
  2. 设置计数器 processed = 0
  3. 从该槽的第一个桶单元开始,遍历其内部定时器链表。
  4. 对于每个定时器,如果已到期且未取消,触发回调,从链表删除,processed++;如果 processed == BATCH_SIZE,记录当前桶和节点位置(断点),结束本轮。
  5. 如果当前桶处理完毕,移到下一个桶继续,直到批次用完或所有桶清空。
  6. 下次 tick 到同一个槽时,从断点继续处理。

这样,每次 tick 的工作量严格受 BATCH_SIZE 控制,保证了时间轮的推进节奏不会被突然的流量高峰打乱。


5. 线程安全要点
  • 生产者-消费者模型:业务线程插入定时器(生产者),专用 tick 线程负责推进和触发(消费者)。
  • 锁粒度:可以为每个槽设置一把锁,或者为每个桶单元设置锁。桶锁粒度更细。
  • 惰性删除:取消定时器时仅打标记,由 tick 线程遍历时真正移除,避免并发删除的复杂性。
  • 对象池:桶单元和定时器节点都从预分配池中获取,消除动态内存分配导致的延迟抖动。

6. Mermaid 结构图

数据结构示意:
#mermaid-svg-UQEBR3QYzsxgFp0D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UQEBR3QYzsxgFp0D .error-icon{fill:#552222;}#mermaid-svg-UQEBR3QYzsxgFp0D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UQEBR3QYzsxgFp0D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UQEBR3QYzsxgFp0D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UQEBR3QYzsxgFp0D .marker.cross{stroke:#333333;}#mermaid-svg-UQEBR3QYzsxgFp0D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UQEBR3QYzsxgFp0D p{margin:0;}#mermaid-svg-UQEBR3QYzsxgFp0D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster-label text{fill:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster-label span{color:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster-label span p{background-color:transparent;}#mermaid-svg-UQEBR3QYzsxgFp0D .label text,#mermaid-svg-UQEBR3QYzsxgFp0D span{fill:#333;color:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D .node rect,#mermaid-svg-UQEBR3QYzsxgFp0D .node circle,#mermaid-svg-UQEBR3QYzsxgFp0D .node ellipse,#mermaid-svg-UQEBR3QYzsxgFp0D .node polygon,#mermaid-svg-UQEBR3QYzsxgFp0D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UQEBR3QYzsxgFp0D .rough-node .label text,#mermaid-svg-UQEBR3QYzsxgFp0D .node .label text,#mermaid-svg-UQEBR3QYzsxgFp0D .image-shape .label,#mermaid-svg-UQEBR3QYzsxgFp0D .icon-shape .label{text-anchor:middle;}#mermaid-svg-UQEBR3QYzsxgFp0D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UQEBR3QYzsxgFp0D .rough-node .label,#mermaid-svg-UQEBR3QYzsxgFp0D .node .label,#mermaid-svg-UQEBR3QYzsxgFp0D .image-shape .label,#mermaid-svg-UQEBR3QYzsxgFp0D .icon-shape .label{text-align:center;}#mermaid-svg-UQEBR3QYzsxgFp0D .node.clickable{cursor:pointer;}#mermaid-svg-UQEBR3QYzsxgFp0D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UQEBR3QYzsxgFp0D .arrowheadPath{fill:#333333;}#mermaid-svg-UQEBR3QYzsxgFp0D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UQEBR3QYzsxgFp0D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UQEBR3QYzsxgFp0D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UQEBR3QYzsxgFp0D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UQEBR3QYzsxgFp0D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UQEBR3QYzsxgFp0D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster text{fill:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D .cluster span{color:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UQEBR3QYzsxgFp0D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UQEBR3QYzsxgFp0D rect.text{fill:none;stroke-width:0;}#mermaid-svg-UQEBR3QYzsxgFp0D .icon-shape,#mermaid-svg-UQEBR3QYzsxgFp0D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UQEBR3QYzsxgFp0D .icon-shape p,#mermaid-svg-UQEBR3QYzsxgFp0D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UQEBR3QYzsxgFp0D .icon-shape .label rect,#mermaid-svg-UQEBR3QYzsxgFp0D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UQEBR3QYzsxgFp0D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UQEBR3QYzsxgFp0D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UQEBR3QYzsxgFp0D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 时间轮数组
slot 0
Bucket 1
slot 1
Bucket 1
Bucket 2
slot 2
...
TimerNode
TimerNode
TimerNode
TimerNode
TimerNode
TimerNode
TimerNode

批次 tick 流程:
#mermaid-svg-ps5QqhbeNChr4Qfs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ps5QqhbeNChr4Qfs .error-icon{fill:#552222;}#mermaid-svg-ps5QqhbeNChr4Qfs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ps5QqhbeNChr4Qfs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ps5QqhbeNChr4Qfs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ps5QqhbeNChr4Qfs .marker.cross{stroke:#333333;}#mermaid-svg-ps5QqhbeNChr4Qfs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ps5QqhbeNChr4Qfs p{margin:0;}#mermaid-svg-ps5QqhbeNChr4Qfs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster-label text{fill:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster-label span{color:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster-label span p{background-color:transparent;}#mermaid-svg-ps5QqhbeNChr4Qfs .label text,#mermaid-svg-ps5QqhbeNChr4Qfs span{fill:#333;color:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs .node rect,#mermaid-svg-ps5QqhbeNChr4Qfs .node circle,#mermaid-svg-ps5QqhbeNChr4Qfs .node ellipse,#mermaid-svg-ps5QqhbeNChr4Qfs .node polygon,#mermaid-svg-ps5QqhbeNChr4Qfs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ps5QqhbeNChr4Qfs .rough-node .label text,#mermaid-svg-ps5QqhbeNChr4Qfs .node .label text,#mermaid-svg-ps5QqhbeNChr4Qfs .image-shape .label,#mermaid-svg-ps5QqhbeNChr4Qfs .icon-shape .label{text-anchor:middle;}#mermaid-svg-ps5QqhbeNChr4Qfs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ps5QqhbeNChr4Qfs .rough-node .label,#mermaid-svg-ps5QqhbeNChr4Qfs .node .label,#mermaid-svg-ps5QqhbeNChr4Qfs .image-shape .label,#mermaid-svg-ps5QqhbeNChr4Qfs .icon-shape .label{text-align:center;}#mermaid-svg-ps5QqhbeNChr4Qfs .node.clickable{cursor:pointer;}#mermaid-svg-ps5QqhbeNChr4Qfs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ps5QqhbeNChr4Qfs .arrowheadPath{fill:#333333;}#mermaid-svg-ps5QqhbeNChr4Qfs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ps5QqhbeNChr4Qfs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ps5QqhbeNChr4Qfs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ps5QqhbeNChr4Qfs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ps5QqhbeNChr4Qfs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ps5QqhbeNChr4Qfs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster text{fill:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs .cluster span{color:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ps5QqhbeNChr4Qfs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ps5QqhbeNChr4Qfs rect.text{fill:none;stroke-width:0;}#mermaid-svg-ps5QqhbeNChr4Qfs .icon-shape,#mermaid-svg-ps5QqhbeNChr4Qfs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ps5QqhbeNChr4Qfs .icon-shape p,#mermaid-svg-ps5QqhbeNChr4Qfs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ps5QqhbeNChr4Qfs .icon-shape .label rect,#mermaid-svg-ps5QqhbeNChr4Qfs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ps5QqhbeNChr4Qfs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ps5QqhbeNChr4Qfs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ps5QqhbeNChr4Qfs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





开始 tick
推进 current_slot 指针
取该槽第一个桶单元
本轮已触发数 < BATCH_SIZE?
桶内还有未处理节点?
触发一个到期定时器
processed++
有下一个桶?
移动到下一个桶
该槽处理完毕
保存断点,停止
结束 tick


7. 总结

这套方案本质上是用空间(桶链表)换时间确定性,用可接受的延迟抖动(批次导致的滞后)换处理时间上界。它保留了固定数组时间轮 O(1) 插入和 O(1) 删除的优点,同时解决了单槽过载带来的长尾延迟问题,非常适合海量定时器、高并发、对延迟抖动容忍度较高的场景(如网络框架超时管理、游戏定时事件)。

相关推荐
行走__Wz7 小时前
【网工入门-eNSP模拟-05】静态路由
网络
xiangw@GZ7 小时前
802.11全系列标准调制编码与速率档对应关系
网络·单片机·嵌入式硬件·架构
Irissgwe7 小时前
数据结构-栈和队列
数据结构·c++·c·栈和队列
两片空白7 小时前
数据容器集合set/frozenset
数据结构
liulilittle7 小时前
KCC:在 BBR 思路上的一次探索
网络·tcp/ip·算法·bbr·通信·拥塞控制·kcc
代码中介商8 小时前
跳表:高效查找的链表黑科技
数据结构
27669582929 小时前
泡泡玛特app 腾讯企业加固/支付宝加固脱修frida rpc调用
网络·网络协议·rpc·frida·泡泡玛特·ppmt·泡泡玛特app-rpc调用
其实防守也摸鱼9 小时前
软件安全与漏洞--Windows底层原理与软件逆向工程基础
linux·网络·数据库·算法·安全·安全架构·软件安全与漏洞
薛定猫AI9 小时前
【深度解析】OpenRouter Fusion API 技术拆解:多模型融合架构的能力边界与工程实践
网络·架构