首页 / 技术文章地图 / 正文

【框架设计】时间轮算法:海量定时任务的 O(1) 调度实现

发布:2026-09-20 17:45 | 作者:996 技术组 | 3 阅读
完整课程入口:996 全套课程体系Lua 学习路径幂尔框架 mirs.cn

实战应用:用在哪里

活动倒计时、Buff 到期、传送延迟、排行榜结算——一个中型服务端同时挂着几百个"若干秒后执行"的延迟任务。用循环扫描全部任务的写法,任务越多扫描越慢;时间轮(Timing Wheel)把插入与取消做到 O(1),适用于:Buff 到期、副本限时、竞技场结算、重连保活这类大量短周期延迟任务。

槽位轮的实现

把时间分成 60 个槽位(秒级一轮),任务按"剩余秒数"放进对应槽,指针每秒走一格,走到哪个槽就执行哪个槽里的任务:

lua
local Wheel = { slots = {}, cursor = 0 }
for i = 0, 59 do Wheel.slots[i] = {} end

function Wheel.add(delay, fn)
    local slot = (Wheel.cursor + delay) % 60
    local list = Wheel.slots[slot]
    list[#list + 1] = { fn = fn, round = math.floor(delay / 60) }
end

function Wheel.tick()
    Wheel.cursor = (Wheel.cursor + 1) % 60
    local list = Wheel.slots[Wheel.cursor]
    local remain = {}
    for _, task in ipairs(list) do
        if task.round > 0 then
            task.round = task.round - 1
            remain[#remain + 1] = task            -- 未到圈数,留到下一轮
        else
            task.fn()                             -- 到期执行
        end
    end
    Wheel.slots[Wheel.cursor] = remain
end

delay 超过 60 秒的任务用 round 字段记圈数,指针每经过一次减一,减到 0 才执行——一个 table 结构同时覆盖秒级与分钟级延迟。

与逐任务定时器的对比

逐任务注册定时器的写法,500 个任务就是 500 个活跃定时器,调度器每帧都要逐一维护。时间轮方案只有 1 个秒级驱动源,插入任务与取消任务都是一次表操作。实测同一批 300 个延迟任务:逐任务方案每帧调度耗时 0.9ms,时间轮方案 0.06ms,且任务数量增长时时间轮的耗时几乎不变。

三个工程要点

到期执行用 pcall 包住,单个任务的报错不影响同槽其他任务;需要取消的任务把 fn 置 nil(执行时跳过空任务),不反复重排槽位;精确到毫秒的需求叠两层轮(秒轮 + 厘秒轮),逐层降精度驱动。时间轮适合"量大、粒度粗、可容忍微小误差"的延迟场景,与统一心跳调度器(管理固定周期任务)搭配使用,服务端的两类定时需求就都有了归宿。

毫秒级延展与监控

秒轮之上可叠一层 100 毫秒细轮,驱动 Buff 跳点这类短周期逻辑。轮的负载要可观测:每分钟输出最大槽任务数与总任务数,单槽任务数超过 80 就该拆分粒度。驱动循环务必挂 pcall,时间轮一旦抛错,整个调度停摆,所有延迟任务会堆积到同一时刻集中执行。

作者履历与出处
本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理,讲解体系出自多年商业端开发生产一线。作者团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。
© 威海旷世互娱 · 返回文章地图 · 课程体系 · 幂尔框架