完整课程入口:996 全套课程体系 | Lua 学习路径 | 幂尔框架 mirs.cn
活动倒计时、Buff 到期、传送延迟、排行榜结算——一个中型服务端同时挂着几百个"若干秒后执行"的延迟任务。用循环扫描全部任务的写法,任务越多扫描越慢;时间轮(Timing Wheel)把插入与取消做到 O(1),适用于:Buff 到期、副本限时、竞技场结算、重连保活这类大量短周期延迟任务。
把时间分成 60 个槽位(秒级一轮),任务按"剩余秒数"放进对应槽,指针每秒走一格,走到哪个槽就执行哪个槽里的任务:
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,时间轮一旦抛错,整个调度停摆,所有延迟任务会堆积到同一时刻集中执行。