MQTT

面试问题:

 1 MQTT 是什么?和 HTTP 有什么区别?: MQTT 是物联网轻量级发布/订阅协议,基于 TCP。 区别: HTTP = 请求/响应(一问一答,短连接,轮询) MQTT = 发布/订阅(设备发到 topic,订阅者收,长连接,实时推送) 专为低带宽、省电、移动/嵌入式设备设计

2 topic 和通配符: + 匹配一级, #匹配多级, sensor/+/+ 匹配 sensor/temp/SN001 , sensor/# 匹配 sensor下所有

3 QoS 0/1/2 区别?QoS0 最多一次:可能丢(最快,适合可丢数据) QoS1 至少一次:可能重复(常用,监控数据选这个) QoS2 恰好一次:最可靠,开销最大(关键指令)

4 Q5: 报文结构?要不要自己解析?报文结构: 固定报头 + 可变报头 + 负载(Payload),  开发用客户端库(Paho),库封装了解析,程序员只处理 payload

5 cleanSession 是什么?true = 不保存会话,断线重连后订阅丢失 false = broker 保存会话,重连恢复订阅 + 收离线消息(配合QoS1/2) 生产建议 false

6 保留消息(Retained)? broker 保存该 topic 最后一条消息,新订阅者一上线立刻收到 用途:设备最新状态/最新温湿度值,不用等设备下次发

7 遗嘱消息(Last Will)? 设备异常断线时,broker 替它发布预先设定好的遗嘱消息 用途:检测设备"非正常离线"(如断电),触发告警

8 KeepAlive 心跳? 客户端定期发 ping,broker 和客户端互相确认存活 超时未收到 → 判定断线 → 触发遗嘱/清理

5000个温湿度传感器通过mqtt发送数据给后端接口实现方案

一、整体架构
温湿度传感器 → 网关 → [自建 EMQX] → 后端 Paho 订阅 → 解析 → 存表 → 预警/联动
   (硬件)      (硬件)    (broker)     (Java 后端)         (sensor_log)  (warn_log)
 
二、给硬件供应商的接入参数(对接文档)

| # | 参数 | 值(示例) | 说明 |
|---|---|---|---|
| 1 | broker 地址 | `tcp://192.168.1.100:1883` | 你 EMQX 的 IP + MQTT 端口 |
| 2 | 账号 | `sensor_01` | 给供应商专用账号,**别给 admin** |
| 3 | 密码 | `********` | 单独生成 |
| 4 | topic | `wms/sensor/temp/{deviceId}` | 设备编号替换 {deviceId},如 SN001 |
| 5 | payload 格式 | `{"deviceId":"SN001","temp":25.3,"humi":60,"time":"2026-09-03 08:00:00"}` | 后端按此解析 |
| 6 | QoS | 1 | 质量等级,别用 0(会丢消息) |
| 7 | 上报频率 | 每 5 分钟一次 | 可调 |
| 8 | 断线重连 | 自动重连 | 必须支持 |
| 9 | 网络 | 能访问你 EMQX 的 IP:1883 | 跨网需 VPN/专线 |
 
后端多线程接收消息代码:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 分片线程池:同一设备的消息固定到同一分片线程 → 组内保序
 */
@Component
public class SensorRoutePool {

    // 分片数 = CPU核数×2(8核→16片,IO密集可再多些)
    private static final int SHARDS =
            Runtime.getRuntime().availableProcessors() * 2;

    // 每片一个"单线程"执行器,有界队列
    private final ExecutorService[] pools;
    private final SensorLogService logService;
    private final WarnService warnService;

    public SensorRoutePool(SensorLogService logService,
                           WarnService warnService) {
        this.logService = logService;
        this.warnService = warnService;
        pools = new ExecutorService[SHARDS];
        for (int i = 0; i < SHARDS; i++) {
            pools[i] = new ThreadPoolExecutor(
                    1, 1,                       // 每片固定单线程
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>(2000),  // 每片有界队列
                    new NamedThreadFactory("sensor-" + i),
                    // 队列满 → 由回调线程自己执行(背压,不丢消息)
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );
        }
    }

    /** MQTT 回调里调用:轻量,立即返回 */
    public void submit(SensorReport r) {
        int shard = Math.floorMod(r.getDeviceId().hashCode(), SHARDS);// 求"数学模",结果一定非负,0到15之间的数,保证设备id一样时返回相同数字
        pools[shard].execute(() -> process(r));
    }

    /** 在分片线程里串行执行,同一设备天然保序 */
    private void process(SensorReport r) {
      // 需要保序的轻量处理放这里(如告警趋势判断) 
    warnService.check(r);
    // 不落库!丢给批量缓冲队列,立即返回
    batchSaver.offer(r);
} }

 

@Component
@Slf4j
public class SensorMqttListener implements MqttCallbackExtended {

    @Autowired
    private MqttProperties props;
    @Autowired
    private SensorInfoService sensorInfoService;
    @Autowired
    private SensorLogService sensorLogService;
    @Autowired
    private WarnService warnService;

    private MqttClient client;

    /** 启动时连接(实现 ApplicationRunner) */
    @Override
    public void run(ApplicationArguments args) {
        connect();
    }

    private void connect() {
        try {
            client = new MqttClient(props.getBroker(), props.getClientId());
            client.setCallback(this);
            MqttConnectOptions opts = new MqttConnectOptions();
            opts.setUserName(props.getUsername());
            opts.setPassword(props.getPassword().toCharArray());
            opts.setCleanSession(false);        // 断线重连不丢订阅
            opts.setAutomaticReconnect(true);   // 自动重连
            opts.setConnectionTimeout(10);
            opts.setKeepAliveInterval(60);      // 心跳保活
            client.connect(opts);
            client.subscribe(props.getSubscribeTopic(), props.getQos());
            log.info("MQTT 已连接并订阅: {}", props.getSubscribeTopic());
        } catch (MqttException e) {
            log.error("MQTT 连接失败,10秒后重试", e);
            // 简单重试;生产可用 @Scheduled 定时检查重连
            Thread.sleep(10000); // 需处理 InterruptedException
            connect();
        }
    }

    /** ★ 收到消息:这是你唯一要处理的业务入口 */
    @Override
    public void messageArrived(String topic, MqttMessage message) {
        SensorReport r = parse(message); 
     sensorRoutePool.submit(r); // 只做哈希+入队,立即返回
} // 连接恢复回调(重连成功后重新订阅) @Override public void connectComplete(boolean reconnect, String serverURI) { try { if (reconnect && client != null) { client.subscribe(props.getSubscribeTopic(), props.getQos()); log.info("MQTT 重连成功并重新订阅"); } } catch (MqttException e) { log.error("重订阅失败", e); } } @Override public void connectionLost(Throwable cause) { log.warn("MQTT 连接断开,将自动重连", cause); } @Override public void deliveryComplete(IMqttDeliveryToken token) { } }

 

@Component
public class SensorBatchSaver {
    private final BlockingQueue<SensorReport> buffer =
            new LinkedBlockingQueue<>(50000);   // LinkedBlockingQueue是基于链表的阻塞队列,Java 并发包里的线程安全队列, 有界,防无限积压

    public void offer(SensorReport r) {
       boolean ok =  buffer.offer(r);     // offer-快速将数据放到队尾,队列满时返回false,put也是将数据放到队尾,队列满时等待,可以加个结果判断

      if (!ok) {
      // 队列满了:降级处理(记录丢弃数/打告警),别阻塞
      }

    }

    @Scheduled(fixedDelay = 1000)               // 每1秒攒一批
    public void flush() {
        List<SensorReport> batch = new ArrayList<>();
        buffer.drainTo(batch, 1000);            // 最多取1000条
        if (!batch.isEmpty()) {
            logMapper.batchInsertIgnore(batch); // 一条SQL插多行
        }
    }
}

 

posted @ 2026-09-02 23:33  杨吃羊  阅读(2)  评论(0)    收藏  举报