为何ThreadPoolTaskScheduler会并发多次执行MQTT心跳定时任务?
问题根因
fixedRate定时任务的堆叠执行特性:你配置PeriodicTrigger时开启了fixedRate=true,该模式下任务执行间隔以上一次任务的开始时间计算,如果出现系统调度延迟、任务执行阻塞的情况,错过的任务会被缓存,等到资源可用时一次性批量触发,这就是你看到几毫秒内连续执行多次run()方法的核心原因。- 旧定时任务未主动销毁,重复运行:触发重连逻辑时会重建Netty Channel和对应的
MqttPingScheduleHandler实例,但旧实例持有的ScheduledFuture没有被主动取消,残留的旧任务和新实例注册的新任务同时运行,进一步放大了重复触发的问题。 - 状态变量线程安全缺陷:标记心跳请求状态的
pingRequestWasSent是普通boolean变量,没有添加volatile修饰,也没有做同步保护,定时任务线程和Netty IO线程之间的变量修改可见性无法保证,可能出现已收到PING响应但状态未同步的情况,导致误判超时。
修复方案
- 替换定时任务执行模式
将fixedRate模式改为更适合心跳场景的fixedDelay模式,该模式以上一次任务的结束时间计算间隔,不会出现任务堆叠的问题:
// 修改pingPeriodicTrigger Bean配置 periodicTrigger.setFixedRate(false);
- 主动销毁旧定时任务
给MqttPingScheduleHandler添加销毁逻辑,在实例销毁、Channel断开时主动取消定时任务,避免任务残留:
@PreDestroy public void stopPingTask() { if (this.future != null && !this.future.isCancelled()) { // 参数true表示如果任务正在运行则中断执行 this.future.cancel(true); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { stopPingTask(); super.channelInactive(ctx); }
- 修复状态变量线程安全问题
给pingRequestWasSent添加volatile修饰,保证多线程下的变量修改可见性:
private volatile boolean pingRequestWasSent;
- 可选优化:增加超时事件防抖
可以在publishPingTimeoutEvent方法中新增状态标记,同一实例只允许发布一次超时事件,直到重连成功后重置状态,避免重复触发重连逻辑。
内容的提问来源于stack exchange,提问作者Max Maximiv
相关产品推荐
相关产品推荐

