MQTT 客户端反复重连怎么办?Spring Boot 接 EMQX 断线重连踩坑实录

先看一段真实的事故现场

下面这段日志摘自我自己项目的真实运行日志(app-run.log,一字未改):

ini 复制代码
08:47:53.028  INFO  --- [main]              正在连接 MQTT Broker: tcp://localhost:1883
08:47:53.695  WARN  --- [thub-server-001] MQTT 连接被拒绝: 无权连接
08:48:03.610  INFO  --- [scheduling-1]    正在连接 MQTT Broker: tcp://localhost:1883
08:48:04.443  INFO  --- [thub-server-001] MQTT 连接成功
08:48:04.451  INFO  --- [thub-server-001] MQTT 已订阅: up/+/+, st/+/+ (reconnect=false)

注意第二行的线程名 [thub-server-001]------这是 Paho 客户端自己的连接线程;第三行的 [scheduling-1] 是 Spring @Scheduled 定时任务线程池的线程。两条"正在连接"来自两个不同的发起方,这正是很多 MQTT 连接抖动问题的根源:不止一个人在抢着连。

MQTT 客户端反复重连的四种典型原因

按我踩过和见过的频率排序:

1. 手动重连和自动重连打架(最容易踩) setAutomaticReconnect(true) 开了自动重连,又自己起一个定时任务轮询调 connect()。Paho 的自动重连线程和你手动发起的连接并发抢同一个 client------同一个 clientId,谁连上谁把对方踢下线(MQTT 同 clientId 互踢规则),表现出来就是"连接成功 → 立刻断开 → 再连接"的死循环。

2. 同一个 clientId 被顶号 两个进程(比如本地调试的服务没关、又启动了一个)用了同一个 clientId 连同一个 Broker,双方互相踢,表现和第 1 种几乎一样。排查方法:打开 EMQX Dashboard → 监控 → 客户端,看同一个 clientId 是不是频繁上下线、或者出现两条会话记录。

3. 认证 / ACL 被拒 密码错、HTTP 认证回调返回格式不对(EMQX 要求 {"result":"allow"},返回自己的统一响应体会被一律判拒)、ACL fail closed。日志特征是稳定报"连接被拒绝"------这种不会抖动,是匀速失败,每 10 秒被拒一次那种。

4. Broker 压根没起来 应用比 EMQX 先启动是开发机的常态(电脑重启后启动顺序不定)。特征是连接超时异常,而不是"被拒绝"。

第 3、4 种都好办,真正让人摸不着头脑的是 1 和 2------因为它们的表现是"明明连上了,怎么又断了"。

我的踩坑与修复:重连权收归一家

我的第一版就是第 1 种:automaticReconnect 开着,同时又写了个 @Scheduled 任务每 10 秒检查连接、没连上就 connect()。平时没事,一旦断线,两个重连机制同时启动,互相把对方踢下线,日志里连接成功和连接断开交替刷屏。

修复思路就一句话:重连权只能有一个主人,且分阶段交接。

  • 还没连上过(应用刚启动、Broker 还没就绪)→ 定时任务兜底重试
  • 成功连上过一次之后 → 定时任务永久退场,断线重连完全交给 Paho

真实代码如下(来源:iot-device-platform/src/main/java/com/iothub/mqtt/MqttConnection.java):

java 复制代码
/** 只要成功连上过一次,后续重连就完全交给 Paho 的 automaticReconnect */
private volatile boolean everConnected = false;

/** 上次发起连接的时间,防止定时任务和自动重连同时抢(同 clientId 会被 EMQX 互踢) */
private volatile long lastConnectAttempt = 0;

/** 只在"从来没连上过"时兜底;连上过之后 automaticReconnect 负责一切 */
@Scheduled(fixedDelayString = "${mqtt.reconnect-interval-ms:10000}")
public void ensureConnected() {
    if (everConnected) {
        return;
    }
    connect();
}

private synchronized void connect() {
    if (client != null && client.isConnected()) {
        return;
    }
    long now = System.currentTimeMillis();
    if (now - lastConnectAttempt < props.getReconnectIntervalMs()) {
        return;   // 节流:两次连接尝试强制间隔,防止高频轰炸 Broker
    }
    lastConnectAttempt = now;
    try {
        if (client == null) {
            client = new MqttAsyncClient(props.getBroker(), props.getClientId(), new MemoryPersistence());
            client.setCallback(callback());
        }
        MqttConnectOptions options = new MqttConnectOptions();
        options.setAutomaticReconnect(true);   // 连上之后掉线,交给 Paho
        options.setCleanSession(true);
        options.setMaxInflight(100);
        options.setConnectionTimeout(10);
        // ... 用户名密码略
        client.connect(options, null, new IMqttActionListener() {
            @Override
            public void onSuccess(IMqttToken asyncActionToken) {
                log.info("MQTT 连接成功");
            }

            @Override
            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                lastConnectAttempt = 0;   // 被拒后清零时间窗,下一轮定时任务可以立刻重试
                log.warn("MQTT 连接被拒绝: {}", exception == null ? "unknown" : exception.getMessage());
            }
        });
    } catch (MqttException e) {
        log.warn("MQTT 连接失败({}),{}ms 后重试", e.getMessage(), props.getReconnectIntervalMs());
    }
}

配套的回调里,connectComplete 负责"交接":

java 复制代码
@Override
public void connectComplete(boolean reconnect, String serverURI) {
    everConnected = true;   // 从此定时任务退场,重连交给 Paho
    try {
        String[] topics = {props.getTelemetryTopic(), props.getStatusTopic()};
        int[] qos = {1, 1};
        IMqttToken token = client.subscribe(topics, qos);
        token.waitForCompletion(10_000);
        log.info("MQTT 已订阅: {} (reconnect={})", String.join(", ", topics), reconnect);
    } catch (MqttException e) {
        log.error("MQTT 订阅失败", e);
    }
}

三个设计点,面试也能聊:

  1. 为什么不启动时连不上就抛异常? 应用先起、Broker 后起是常态。服务"不因依赖缺失而起不来",连接失败只告警,由定时任务保证最终连上------这正是"最终成功"的思路。
  2. 为什么重连后必须重新订阅? 我用了 cleanSession=true:会话不保留,重连后订阅关系是空的,不在 connectComplete 里补订阅,就出现"连接显示在线、数据一条都收不到"的灵异现象。
  3. everConnected 为什么要 volatile? 定时任务线程和 Paho 回调线程是两个线程,一个写一个读,可见性必须保证。

怎么验证重连真的可靠

光看代码没用,重连这种东西必须现场演练一次。我的验证步骤(可复现):

  1. 起应用,等日志出现"MQTT 已订阅",EMQX Dashboard 客户端页能看到 iothub-server-001 在线;
  2. bin\emqx stop 停掉 Broker → 应用日志出现"MQTT 连接断开"(connectionLost);
  3. bin\emqx start 再启动,不重启应用 → 日志自动出现"MQTT 已订阅: ... (reconnect=true)";
  4. 回到看板,实时曲线继续往前走,数据链路无感恢复。

排查清单(拿去直接用)

现象 大概率原因 怎么确认
稳定报"连接被拒绝" 认证/ACL 配置错 核对用户名密码;HTTP 认证回调必须返回 {"result":"allow"}
疯狂刷"正在连接",连上又断 手动重连与自动重连互踢 全局搜 connect(,确保只有一个地方发起重连
连接成功几秒后被踢,循环 同 clientId 顶号 EMQX Dashboard 看该 clientId 是否频繁上下线;查有没有第二个进程
启动时报连接失败,之后正常 Broker 比应用后启动 属正常,定时任务兜底即可
显示在线但收不到数据 重连后没重新订阅 在 connectComplete 里补订阅

三个常见追问

Q:Paho 自动重连的间隔能自己控制吗? A:不能配置,它内部从 1 秒开始翻倍、2 分钟封顶。想要可控的退避节奏,就得关掉 automaticReconnect 自己实现------但切记全工程只留这一个重连入口,别像第一版的我那样两头抢。

Q:cleanSession 该设 true 还是 false? A:看你要不要"断线补传"。我的服务端消费遥测数据,丢了靠设备端本地缓存 + 时间戳重连后补发,服务端按时间戳幂等去重,所以 true 更简单。要 Broker 帮忙暂存离线消息就得 false,代价是会话状态管理复杂度上一个台阶。

Q:keepalive 设多少合适? A:Paho 默认 60 秒,服务端固定网络够用;弱网设备可以适当加大,配合 Broker 侧的会话过期时间一起调。keepalive 不是越小越"实时",它只是探活,实时性靠的是上报频率和推送链路。


我是软件工程在读(专升本),正在从零搭一个物联网设备接入平台(Spring Boot + EMQX + MySQL),本文代码全部来自这个真实项目。踩坑记录会持续更新,欢迎关注,一起卷物联网后端。

相关推荐
Q26433650232 小时前
【有源码】基于SpringBoot+Vue的博物馆展览与服务一体化平台-java博物馆数字化展品展示与交互平台设计
java·vue.js·spring boot·spring·毕业设计·javaweb·课程设计
专业程序开发源2 小时前
springboot全民健身和饮食健康管理系统29158-计算机课程设计、毕业设计
java·spring boot·后端·python·django·php·课程设计
ym hyd 1112 小时前
汽车零部件缺陷管理系统源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·汽车·毕设
常州晟凯电子科技3 小时前
sigmaStar SSC8836Q/SSC8826Q开发笔记之环境搭建和SDK编译
笔记·嵌入式硬件·物联网·视频编解码
paopaokaka_luck3 小时前
高校社团管理(AI辅助任务分配、协同过滤算法推荐、ECharts数据可视化、活动参与闭环、校园交流与反馈、器材借还管理)
java·前端·javascript·spring boot·数据分析·echarts
沐欣工作室_lvyiyi3 小时前
基于ML307A物联网通信技术在智能家居中的应用研究(论文+源码)
物联网·智能家居·ml307a 4g通信模块
vx_Biye_Design3 小时前
springboot小区管理系统68491-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
专业程序开发源4 小时前
springboot外卖系统94294-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
FYKJ_20104 小时前
springboot心理健康管理系统86254-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·mysql·django·课程设计