在百万级并发连接的网络服务中,心跳检测是维持连接可用性、及时清理无效连接的核心机制。如果采用传统的定时任务调度方案,每一条连接都需要创建一个独立的定时任务,会占用大量线程和内存资源,很容易导致系统性能下降甚至崩溃。Netty内置的HashedWheelTimer基于时间轮算法实现,能够以极低的资源消耗高效管理海量定时任务,非常适合用于此类场景的心跳检测优化。

HashedWheelTimer 核心原理
HashedWheelTimer的核心是时间轮结构,时间轮可以理解为一个环形数组,每个数组元素(槽位)对应一个时间间隔,每个槽位中存放的是到期时间在该槽位对应的时间范围内的定时任务。时间轮会以固定的频率转动,每次转动到对应的槽位时,就执行该槽位中的所有到期任务。
时间轮的转动由一个工作线程驱动,所有定时任务的添加、执行都在该线程中完成,避免了多线程竞争带来的开销。同时时间轮采用哈希的方式将任务分配到不同的槽位,任务添加和取消的时间复杂度都是O(1),非常适合高并发场景。
时间轮的核心参数
- tickDuration:时间轮每个槽位的时间间隔,也就是时间轮转动一次的时间
- ticksPerWheel:时间轮的槽位数量,决定了时间轮的精度和可容纳的最大时间范围
- leakDetection:是否开启内存泄漏检测,生产环境建议开启
传统心跳检测方案的痛点
在百万级连接场景下,常见的传统心跳检测方案有两种:
- 为每个连接创建一个定时任务,使用JDK自带的ScheduledExecutorService调度,这种方式会创建大量定时任务对象,线程池也会创建大量线程处理任务,内存和CPU开销极大。
- 使用一个定时任务扫描所有连接的心跳时间,这种方式在连接数量很大时,每次扫描都会遍历所有连接,耗时很长,而且如果有连接心跳到期,处理不及时会影响整体性能。
而HashedWheelTimer只需要一个工作线程,就可以管理所有连接的心跳定时任务,所有任务的添加和取消都是O(1)复杂度,不会随着连接数增加而带来明显的性能下降。
基于 HashedWheelTimer 实现心跳检测的步骤
1. 创建 HashedWheelTimer 实例
首先需要根据业务场景初始化HashedWheelTimer,合理设置槽位数量和每个槽位的时间间隔,示例代码如下:
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import java.util.concurrent.TimeUnit;
public class HeartbeatTimerManager {
// 时间轮每个槽位100毫秒,总共512个槽位,最大可表示的时间范围是512*100ms=51.2秒
// 如果心跳间隔超过这个时间范围,时间轮会自动扩展,将任务放到后续的轮次中
private static final Timer HEARTBEAT_TIMER = new HashedWheelTimer(
r -> {
Thread t = new Thread(r, "heartbeat-timer-thread");
t.setDaemon(true);
return t;
},
100,
TimeUnit.MILLISECONDS,
512
);
public static Timer getHeartbeatTimer() {
return HEARTBEAT_TIMER;
}
}
2. 连接建立时添加心跳定时任务
当客户端和服务器建立连接后,为该连接添加一个心跳检测定时任务,任务的逻辑是检查连接上一次心跳的时间,如果超过阈值就关闭连接。示例代码如下:
import io.netty.channel.Channel;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class HeartbeatHandler {
// 心跳间隔是10秒,超过15秒没有收到心跳就关闭连接
private static final long HEARTBEAT_INTERVAL = 10;
private static final long HEARTBEAT_TIMEOUT = 15;
public static void addHeartbeatTask(Channel channel) {
Timer timer = HeartbeatTimerManager.getHeartbeatTimer();
// 初始延迟10秒后执行第一次心跳检查,之后每隔10秒执行一次
timer.newTimeout(new HeartbeatTimeoutTask(channel), HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
}
static class HeartbeatTimeoutTask implements TimerTask {
private final Channel channel;
public HeartbeatTimeoutTask(Channel channel) {
this.channel = channel;
}
@Override
public void run(Timer timer) throws Exception {
// 获取连接上一次收到心跳的时间,这里需要根据实际存储方式获取,比如存在Channel的属性中
Long lastHeartbeatTime = channel.attr(ConnectionAttr.LAST_HEARTBEAT_TIME).get();
long currentTime = System.currentTimeMillis();
// 如果超过15秒没有收到心跳,关闭连接
if (lastHeartbeatTime == null || currentTime - lastHeartbeatTime > HEARTBEAT_TIMEOUT * 1000) {
channel.close();
return;
}
// 如果连接还存活,重新添加一个10秒后的心跳检查任务
timer.newTimeout(this, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
}
}
}
3. 收到心跳时更新时间戳
当服务器收到客户端的心跳消息时,更新该连接的上一次心跳时间,示例代码如下:
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ClientHeartbeatHandler extends SimpleChannelInboundHandler<HeartbeatMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HeartbeatMessage msg) throws Exception {
Channel channel = ctx.channel();
// 更新上一次心跳时间为当前时间
channel.attr(ConnectionAttr.LAST_HEARTBEAT_TIME).set(System.currentTimeMillis());
// 返回心跳响应
ctx.writeAndFlush(new HeartbeatResponseMessage());
}
}
4. 连接关闭时取消定时任务
HashedWheelTimer的定时任务返回Timeout对象,我们可以通过该对象取消未执行的任务,避免连接关闭后任务仍然执行。修改添加任务的代码如下:
import io.netty.channel.Channel;
import io.netty.util.Timer;
import io.netty.util.Timeout;
public class HeartbeatHandler {
private static final long HEARTBEAT_INTERVAL = 10;
private static final long HEARTBEAT_TIMEOUT = 15;
public static void addHeartbeatTask(Channel channel) {
Timer timer = HeartbeatTimerManager.getHeartbeatTimer();
HeartbeatTimeoutTask task = new HeartbeatTimeoutTask(channel);
// 保存Timeout对象到Channel的属性中
Timeout timeout = timer.newTimeout(task, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
channel.attr(ConnectionAttr.HEARTBEAT_TIMEOUT).set(timeout);
}
public static void cancelHeartbeatTask(Channel channel) {
Timeout timeout = channel.attr(ConnectionAttr.HEARTBEAT_TIMEOUT).get();
if (timeout != null) {
timeout.cancel();
}
}
static class HeartbeatTimeoutTask implements TimerTask {
private final Channel channel;
public HeartbeatTimeoutTask(Channel channel) {
this.channel = channel;
}
@Override
public void run(Timer timer) throws Exception {
Long lastHeartbeatTime = channel.attr(ConnectionAttr.LAST_HEARTBEAT_TIME).get();
long currentTime = System.currentTimeMillis();
if (lastHeartbeatTime == null || currentTime - lastHeartbeatTime > HEARTBEAT_TIMEOUT * 1000) {
channel.close();
return;
}
// 重新添加任务时更新保存的Timeout对象
Timeout newTimeout = timer.newTimeout(this, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
channel.attr(ConnectionAttr.HEARTBEAT_TIMEOUT).set(newTimeout);
}
}
}
参数调优建议
要让HashedWheelTimer在百万级连接场景下发挥最佳性能,需要根据业务场景调整参数:
| 参数 | 调优建议 |
|---|---|
| tickDuration | 建议设置为心跳间隔的1/10到1/5,比如心跳间隔10秒,可以设置为100-2000毫秒,太小会增加时间轮转动的频率,太大任务执行精度会下降 |
| ticksPerWheel | 建议设置为时间轮最大可表示时间范围 / tickDuration,最大可表示时间范围建议大于等于心跳超时时间,避免任务被多次哈希到不同轮次 |
同时需要注意,HashedWheelTimer的工作线程是单线程,所以定时任务中的逻辑不能执行耗时操作,否则会阻塞时间轮的转动,影响其他任务的执行。如果心跳检查逻辑比较复杂,可以提交到业务线程池执行,工作线程只负责判断是否需要执行检查逻辑。
注意事项
- HashedWheelTimer的定时任务执行是在工作线程中的,不要在任务中执行阻塞操作,否则会影响整个时间轮的调度。
- 如果心跳间隔超过时间轮的最大可表示时间范围,时间轮会自动将任务放到后续的轮次中,不需要额外处理,但是要注意任务的超时时间计算要准确。
- 生产环境中建议开启Netty的内存泄漏检测,避免因为任务未正确取消导致的内存泄漏问题。
- 连接关闭时一定要取消对应的定时任务,避免无效任务继续执行占用资源。
NettyHashedWheelTimer心跳检测百万级并发连接时间轮算法修改时间:2026-06-20 02:18:48