时间轮与 IdleStateHandler
Netty 的时间轮适合海量、精度要求不是纳秒级的超时任务;IdleStateHandler 则把连接读写活动转换为定时事件。两者结合后,心跳、连接保活和空闲断开都能复用 EventLoop 的调度能力。
先给答案:时间轮把大量定时任务从“逐个计时”变成“按槽批量检查”
Section titled “先给答案:时间轮把大量定时任务从“逐个计时”变成“按槽批量检查””连接空闲检测的任务数量通常与连接数同量级,如果每个任务都独立维护精确计时器,调度结构会承受大量插入、删除和唤醒成本。时间轮把过期时间映射到槽位,用 remainingRounds 表示还要绕几圈;每次 tick 只处理当前槽中的任务。
这意味着 IdleStateHandler 发现的是“超过阈值的状态”,不是业务请求已经失败。读空闲、写空闲和读写都空闲分别对应不同的心跳或断开策略;处理器还必须考虑事件触发后连接是否继续存活,以及任务取消时槽位中残留引用如何清理。
tick=10 0 1 2 3 4 5 6 7 ▲ current
timeout(deadline=42) calculated=42 / tickDuration stopIndex = calculated & mask remainingRounds = (calculated - tick) / wheel.lengthHashedWheelTimer#start 使用 CAS 把 worker 从 INIT 置为 STARTED,并等待 worker 初始化起始时间,坐标为 common/src/main/java/io/netty/util/HashedWheelTimer.java:352-376。新增 timeout 不直接修改时间轮,而是进入 MPSC 队列;worker 每 tick 最多搬运 100000 个任务,计算槽位和轮数,坐标为 HashedWheelTimer.java:522-544。
每个槽位是双向链表。到达 tick 时,若 remainingRounds <= 0 且 deadline 已到就执行;否则轮数减一,坐标为 HashedWheelTimer.java:760-802。
为什么需要 remainingRounds
Section titled “为什么需要 remainingRounds”只用 deadline & mask 会把“下一圈”和“几十圈之后”的任务放进同一个槽,无法知道当前 tick 是否真的到期。remainingRounds 把槽位定位和圈数判断拆开,代价是每个 timeout 多一个 long,并且每圈都要检查一次。
替代方案是最小堆。堆能更精确地按 deadline 排序,但每次插入和取消都有 O(log n) 成本;时间轮把常见插入路径降到近似 O(1),适合连接超时、重试和海量短任务。
IdleStateHandler
Section titled “IdleStateHandler”handlerAdded/channelActive │ initialize ├─ schedule reader task ├─ schedule writer task └─ schedule all-idle task │ ▼ fireUserEventTriggered构造函数把非正数时间视为禁用,正数转换为纳秒并设置最小超时,坐标为 handler/timeout/IdleStateHandler.java:192-214。Handler 加入已激活 Channel 后会立即初始化,坐标为 IdleStateHandler.java:240-250;具体任务通过 ctx.executor().schedule 调度,坐标为 IdleStateHandler.java:350-365。
observeOutput 开启后,Handler 会比较 ChannelOutboundBuffer.current()、pending bytes 和 flush progress,坐标为 IdleStateHandler.java:411-420,因此“调用过 write 但数据仍在缓冲区没有进展”可以区别于真正的写空闲。
- 时间轮的 tick 精度不是任务的精确执行时间;EventLoop 忙或系统调度延迟时,任务会晚执行。
IdleStateHandler只产生事件,不会自动关闭连接;是否发心跳或 close 由业务 Handler 决定。- 同一个 Handler 被错误地跨 Channel 复用,会把读写时间状态混在一起;除非明确满足共享约束,不要声明
@Sharable。 HashedWheelTimer#stop不能从 worker 线程调用,且会返回未处理 timeout,坐标为HashedWheelTimer.java:379-428。
时间轮适合把大量定时任务压缩到“槽位 + 轮数”两个维度;生产实现还要考虑取消队列、单 tick 搬运上限和关闭时的未处理任务回收。它可用于重试、租约、会话过期和连接保活。
面试锚点:时间轮为什么需要 remainingRounds?IdleStateHandler 如何判断写操作是否真的有进展?