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发送数据给后端接口实现方案
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插多行 } } }
浙公网安备 33010602011771号