想象一个没有固定门牌的老城区:家家户户靠蜂鸟邮局收发消息。有人往“温度计/三楼”的信箱投一条报数,另一户登记过这个信箱,蜂鸟就把纸条送去;有人远行前会在柜台留一个密封信封,如果出发后失联,邮局就在他的信箱里代发“此人失联”的讣告。这套秩序,就是 MQTT。MQTT 是一种为低带宽、弱网、海量设备设计的发布/订阅消息协议:设备只和 Broker 保持一条 TCP 连接,就能完成“一对多”消息分发;它用主题(Topic)组织消息、用 0/1/2 三级服务质量控制可靠投递、用保留消息和遗嘱感知离线。在流式数据处理链路中,MQTT 常作为“最前端”的物联网接入层,把现场数据汇入 Kafka、Spark 等下游系统。本文将以蜂鸟邮局为喻,从协议概念讲到选型与工程实践,并给出一份可运行的 Python 模拟器。

岗位关联

理解 MQTT,等于拿到物联网消息接入层的通用钥匙;无论做车联网、智能工厂还是智慧城市,都能快速选型与排障。

目录
  1. 可视化演示
  2. 协议概览:邮局解决的不只是送信
  3. Topic主题:门牌编号与订阅规则
  4. 服务质量QoS:平信、挂号与面签
  5. 保留消息与遗嘱:公告板与遗言信封
  6. 会话与心跳:储物柜与报到约定
  7. 关键算法:高峰期排队、去重与重试
  8. 选型对比:为什么是MQTT而不是Kafka
  9. 工程实践:把邮局开进流式数据处理中心
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

协议概览:邮局解决的不只是送信

老城区没有门牌,邻里间全靠蜂鸟邮局互相传话:你要给王奶奶送一句“今晚停电”,只需把纸条丢进对应的信箱,蜂鸟就会完成投递。这种“我不认识你,但我的消息能找到你”的默契,就是发布/订阅模型。

在没有中心邮局之前,假设有 \(P\) 个发布者和 \(S\) 个订阅者,每个发布者都要知道所有订阅者的地址,总连接数是 \(P \times S\)。有了蜂鸟邮局后,每个人只需要和邮局建立一条连接,总数变成 \(P + S\)。这个“多对多转成一对多”的转变,正是 MQTT 的核心价值:Broker 充当邮局,所有客户端只连接它,由它负责按主题路由。

MQTT 是 OASIS 标准下的轻量级消息协议,默认跑在 TCP 上,端口 1883。它的二进制报文头非常小,适合单片机、传感器这类“计算力有限、电池珍贵、网络不稳定”的设备。在流式数据处理场景中,MQTT 通常承担最前端接入层,负责把海量设备消息汇入数据中心。

所以蜂鸟邮局真正解决的不是“送得快”,而是“让老街坊不必互相记门牌”。发布者不用关心谁在听,订阅者也不用关心谁在说,邮局在中间把两边解耦开。

Cdirect=P×S,Cmqtt=P+S\displaystyle C_{\text{direct}}=P \times S,\qquad C_{\text{mqtt}}=P+S
  1. 发布者数量 \(P\)代表产生消息的设备个数,相当于投递纸条的居民。
    PP
  2. 订阅者数量 \(S\)代表关心消息的收件人数,相当于登记收信的居民。
    SS
  3. 乘法 \(P \times S\)没有邮局时,每个发布者都要给每个订阅者单独送一次,连接数会爆炸式增长。
    P×SP \times S
  4. 加法 \(P+S\)有了 Broker 后,每个客户端只维护一条到邮局的连接,总数是线性相加。
    P+SP+S

Topic主题:门牌编号与订阅规则

蜂鸟邮局收信时不看人名,只看信箱上的分类标签,比如“李府/厨房/温度”。这样的标签用斜杠分成多级,就是 MQTT 里的 Topic(主题)。主题不是预先建好的“队列”,而是轻量的字符串路径,发布者写到哪,订阅者就听哪。

主题路径以 / 分层,相当于地址里的“小区/楼栋/房间”。订阅者可以用通配符表达“一组信箱”:+ 匹配任意一层,# 匹配剩余所有层。比如订阅 工厂/+/温度,能收到 工厂/一车间/温度,也能收到 工厂/二车间/温度,但收不到 工厂/一车间/湿度。

这个设计让路由变得非常灵活:一个传感器发布 factory/line1/temperature,所有订阅 factory/# 的下游消费者都能收到,也可以只订阅 factory/+/temperature 过滤掉其他类型。主题的层级越清晰,Broker 的分拣就越高效;反之,把主题画成一张大平层,会让邮差蜂鸟迷路。

给主题命名时要像写地址:从通用到具体、避免使用空格和控制字符、控制层级深度。好的主题设计能让 Broker 的匹配树更浅,也能让团队在调试时一眼看懂消息来自哪台设备、哪种读数。

d=nslash+1\displaystyle d = n_{\text{slash}} + 1
  1. 斜杠数量 \(n_{\text{slash}}\)表示主题路径中的分隔符个数,相当于门牌里“/”的个数。
    nslashn_{\text{slash}}
  2. 加一 \(d = n_{\text{slash}}+1\)根级也算一层,所以主题深度等于分隔符数量加一。
    d=nslash+1d = n_{\text{slash}}+1
  3. 井号匹配剩余层当过滤器某一段为 # 时,剩余层级全部匹配,不再继续比较。
    #\#

服务质量QoS:平信、挂号与面签

居民们寄信时有三种选择:平信、挂号、面签。平信丢了就丢了,挂号保证送到但可能重复,面签则要求收件人当面确认且不重复。MQTT 对每一条消息也有三个服务质量等级:QoS 0、QoS 1、QoS 2。

QoS 0 是“最多一次”,发布者发完就忘,不等待任何确认,适合传感器高频采样,丢一个点影响不大。QoS 1 是“至少一次”,发布者等待 Broker 回 PUBACK,如果没等到就重发,所以接收方可能收到重复消息。QoS 2 是“恰好一次”,通过四步握手保证不重不丢,代价是报文数量最多、延迟最高。

实际选型时千万不要“无脑上 QoS 2”。在弱网环境下,QoS 2 的重传会导致严重的头部阻塞;而设备状态上报这类数据,QoS 0 已经足够。可以把 QoS 想象成寄信方式:普通住户报菜价用平信,汇款单用面签,选错方式不是多花钱就是丢件。

QoS 是 MQTT 中最容易被误用的参数。记住一个原则:可靠不等于“等级越高越好”,而是“刚好覆盖业务风险”。温度上报丢一条无所谓,但“打开阀门”的指令一旦丢掉就可能出安全事故,这时才需要 QoS 1 或 2。

Cq={1,q=02,q=14,q=2\displaystyle C_{q}=\begin{cases}1, & q=0\\2, & q=1\\4, & q=2\end{cases}
  1. \(C_q\) 表示报文开销即完成一次消息投递需要的最少报文数量,代表“寄信成本”。
    CqC_q
  2. \(q=0\) 时成本为 1只发 PUBLISH,没有确认,成本最低。
    q=0, Cq=1q=0,\ C_q=1
  3. \(q=1\) 时成本为 2PUBLISH 加一条 PUBACK,能确认收到但可能重复。
    q=1, Cq=2q=1,\ C_q=2
  4. \(q=2\) 时成本为 4PUBLISH、PUBREC、PUBREL、PUBCOMP 四条报文完成协商,保证不重不丢。
    q=2, Cq=4q=2,\ C_q=4

保留消息与遗嘱:公告板与遗言信封

邮局大厅里有一块公告板,上面贴着“最近一班末班车时间”。新来的居民不需要翻历史记录,看一眼公告板就能拿到最新信息。MQTT 的保留消息(Retained Message)就是这个公告板:发布者发送消息时把 retain 位置 1,Broker 会存下“该主题最新的一条”,下次有订阅者订阅该主题时,立刻把这条消息推给他。

遗嘱消息(Will Message)则像是出发前寄存在邮局的遗言信封。客户端在建立连接时把遗嘱主题、内容、QoS 一起告诉 Broker;如果之后客户端异常断开(比如断电、断网),Broker 会自动把这个信封投递到对应主题;如果是客户端主动正常断开,遗嘱就会被撤销。

这对物联网设备状态感知非常有用:设备上线时发布一条 设备/在线 的保留消息,订阅者新上线也能立刻知道哪些设备在线;设备配置遗嘱 设备/离线,一旦断网,监控系统马上收到报警,不用等超时才判断失联。

公告板上的内容只有“最近一条”,不是历史全量;所以保留消息适合做状态快照,不适合做日志存储。遗嘱消息则是“意外预案”,它能大幅缩短异常感知时间,是 MQTT 对比普通 HTTP 轮询的一大亮点。

W=⟨tw, mw, qw, rw⟩\displaystyle W = \langle t_w,\ m_w,\ q_w,\ r_w \rangle
  1. 遗嘱主题 \(t_w\)遗嘱消息要投递到的 Topic,相当于信封上的收件地址。
    twt_w
  2. 遗嘱内容 \(m_w\)遗嘱消息的载荷,比如“传感器 01 失联”。
    mwm_w
  3. 遗嘱 QoS \(q_w\)代发遗嘱时的服务质量等级,决定投递可靠性。
    qwq_w
  4. 遗嘱保留标志 \(r_w\)代发遗嘱时是否同时写入保留公告板。
    rwr_w

会话与心跳:储物柜与报到约定

蜂鸟邮局会给常驻客户保留一个储物柜,里面的订阅关系放在柜子里。如果客户去了外地,柜子不会立刻清空,等他回来还能继续使用。但邮局也会担心客户已经搬走,所以双方约定:每隔一段时间必须来报到一次,超过时限没出现,就认为失联。

MQTT 用 Clean Session 标志控制储物柜的去留。Clean Session=1 表示“每次连接都是新开始”,断开后服务端立刻清空会话;Clean Session=0 表示“本次连接使用持久会话”,Broker 会保存订阅关系和离线期间的 QoS 1/2 消息,设备重连后自动补投。实际工程中,低功耗设备往往用 Cleansession=0 实现“离线不错过指令”。

KeepAlive 是双方的心跳约定:客户端在 KeepAlive 秒内如果没有发送任何报文,就要主动发 PINGREQ;Broker 如果在超过 1.5 倍 KeepAlive 时间里没有收到任何报文,则认为连接已死,关闭连接并触发遗嘱。这个“1.5 倍”的宽容窗口,是为了容忍网络抖动,就像蜂鸟允许客户迟到半小时,但超过太久就会处理退信。

会话和心跳共同构成了“连接可靠性”的底座。持久会话让设备断网重连后无缝衔接,而合理的心跳间隔既不能太短导致误判,也不能太长导致故障发现延迟。

offline  ⟺  tnow−tlast>1.5×K\displaystyle \text{offline} \iff t_{\text{now}} - t_{\text{last}} > 1.5 \times K
  1. 当前时间 \(t_{\text{now}}\)Broker 判断连接状态时的系统时间。
    tnowt_{\text{now}}
  2. 最后通信时间 \(t_{\text{last}}\)Broker 最后一次收到该客户端报文的时间。
    tlastt_{\text{last}}
  3. 心跳间隔 \(K\)客户端与 Broker 约定的 KeepAlive 数值,单位是秒。
    KK
  4. 超时判定 \(1.5 \times K\)时间差超过 1.5 倍心跳间隔,就判定客户端离线。
    tnow−tlast>1.5×Kt_{\text{now}} - t_{\text{last}} > 1.5 \times K

关键算法:高峰期排队、去重与重试

逢年过节,蜂鸟邮局会迎来投递高峰:队尾越排越长,但所有蜂鸟不能同时扑向同一个报箱,否则会撞车。MQTT 工程中同样需要处理三类关键问题:消息去重、背压限流、断线重试。

QoS 2 的“恰好一次”依赖消息标识符和四步握手去重:发送方 PUBLISH,Broker 回 PUBREC 确认已收到,发送方再发 PUBREL 表示“确认你的确认”,Broker 回 PUBCOMP 结束。这套状态机保证即使 PUBLISH 重复到达,Broker 也不会重复投递。背压则出现在下游消费速度跟不上生产速度时,Broker 或网关需要限流、堆积或丢弃旧消息,避免把整个系统拖垮。

客户端断线重连最忌讳“一窝蜂”:所有设备同时重连会造成连接风暴。标准做法是指数退避:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒……再加上随机抖动,让每次重试彼此错开。蜂鸟不会同时冲向同一户,隔一段时间再试反而更快。

去重、背压、退避,本质上都是“在不确定的网络里,用有节制的重试换取整体稳定”。真正可靠的系统不是不犯错,而是犯错后能优雅地恢复。

tn=tbase×2n+jitter\displaystyle t_n = t_{\text{base}} \times 2^{n} + \text{jitter}
  1. 基准延迟 \(t_{\text{base}}\)第一次重试等待的基础时间,通常设为 1 秒。
    tbaset_{\text{base}}
  2. 连续失败次数 \(n\)从上次成功连接后连续失败的次数,用于放大等待时间。
    nn
  3. 指数放大 \(2^{n}\)每次失败后重试时间翻倍,避免所有客户端同时冲击。
    2n2^{n}
  4. 随机抖动 jitter在指数结果上叠加一个随机小量,让同批次设备各自错峰。
    jitter\text{jitter}

选型对比:为什么是MQTT而不是Kafka

老街坊们最近在讨论:运大件货物用拖拉机,送日常小纸条用蜂鸟。消息中间件也一样,MQTT 与 Kafka、RabbitMQ 各有各的主场。

Kafka 是“拖拉机”:它擅长海量日志、高吞吐、离线回放,消息落地到磁盘,分区可以并行消费,但客户端和网络开销都更重,不适合电池供电的嵌入式设备。RabbitMQ 是“企业调度车”:支持复杂路由和多种消息模型,但协议开销比 MQTT 大,弱网表现也不如 MQTT 稳定。

选型决策可以这样理解:设备侧接入、低功耗、弱网、频繁断线,优先 MQTT;数据进入数据中心后做流式计算、离线重放,优先 Kafka;企业内应用系统之间的复杂路由,优先 RabbitMQ。在真实的大数据管道里,它们经常组合使用:MQTT 负责最前端的物联网设备接入,Kafka 负责中心化的流式数据缓冲。

MQTT 的固定报头最小只有 2 字节,而 HTTP 的请求头动辄上百字节,所以同样的带宽下,MQTT 能传更多有效数据。选型不是比谁更先进,而是比谁更适配场景。

η=SpayloadSpayload+Oprotocol\displaystyle \eta = \frac{S_{\text{payload}}}{S_{\text{payload}} + O_{\text{protocol}}}
  1. 消息体大小 \(S_{\text{payload}}\)实际业务数据的大小,比如“36.5”这个温度读数。
    SpayloadS_{\text{payload}}
  2. 协议开销 \(O_{\text{protocol}}\)为了传输这条业务数据额外付出的报文头、确认包等字节数。
    OprotocolO_{\text{protocol}}
  3. 传输效率 \(\eta\)有效数据字节占总传输字节的比例,\(\eta\) 越高,说明协议越轻量。
    η=SpayloadSpayload+Oprotocol\eta = \frac{S_{\text{payload}}}{S_{\text{payload}} + O_{\text{protocol}}}

工程实践:把邮局开进流式数据处理中心

蜂鸟邮局要扩容、要安全,还要和城市总仓的数据中心打通。MQTT 的工程实践同样围绕三个词:Broker 选型、安全认证、数据管道。

Broker 选型上,Mosquitto 轻量且适合嵌入式单机,EMQX 支持千万级连接和海量主题,HiveMQ 提供企业级运维能力;中小规模设备可以选择云厂商托管的 MQTT 服务。部署时推荐用集群+桥接架构:不同区域部署边缘 Broker,再桥接汇总到中心 Broker,形成分层邮局网络。

安全方面,生产环境必须启用 TLS 加密传输,使用用户名密码或证书双向认证,并通过 ACL 控制每个客户端能发布/订阅哪些主题。数据管道上,经典流式架构是:传感器 → MQTT Broker → Kafka → Spark/Flink。MQTT 负责海量设备接入,Kafka 负责下游流式计算的缓冲与重放,两者互补。

蜂鸟驿站运行良好,离不开分拣流程、安保和通往总仓的传送带。MQTT 工程化也是如此:选对 Broker、做好安全、打通数据管道,才能真正把物联网数据变成流式处理的价值流。

TPSmax⁡=BnetSmsg×Rdup\displaystyle TPS_{\max} = \frac{B_{\text{net}}}{S_{\text{msg}} \times R_{\text{dup}}}
  1. 有效带宽 \(B_{\text{net}}\)实际可用于业务数据的网络带宽,单位如 Mbps。
    BnetB_{\text{net}}
  2. 平均消息大小 \(S_{\text{msg}}\)每条消息的总字节数,包括报文头和业务数据。
    SmsgS_{\text{msg}}
  3. 重传因子 \(R_{\text{dup}}\)考虑 QoS 重传、丢包重发带来的额外倍数,通常大于 1。
    RdupR_{\text{dup}}
  4. 最大吞吐 \(TPS_{\max}\)在给定带宽、消息大小和重传代价下,系统每秒最多能处理的消息条数。
    TPSmax⁡=BnetSmsg×RdupTPS_{\max} = \frac{B_{\text{net}}}{S_{\text{msg}} \times R_{\text{dup}}}

AI 应用场景

智能工厂设备数据采集

阶段:数据接入与预处理

解决的问题:传统工厂用 PLC 点对点采集,接线复杂、协议私有,设备一多就难以扩展。MQTT 让每个传感器通过 WiFi/5G 接入统一 Broker,AI 平台用订阅者身份消费数据流。

具体案例:某汽车产线在机器人关节上安装振动传感器,以 10Hz 频率上报 factory/robot/joint/vibration,EMQX 集群接入后实时转发给 Flink,AI 模型用流式数据预测刀具磨损,提前发出维护工单。

车联网指令下发与状态回传

阶段:实时决策与指令执行

解决的问题:车辆在移动网络中频繁切换基站,普通 HTTP 长轮询容易断连。MQTT 的持久会话让车辆离线时指令不丢,遗嘱消息让云端快速感知车辆失联。

具体案例:某新能源车企用 MQTT 向车辆推送 OTA 升级指令,指令走 QoS 1,车辆每 30 秒心跳上报;一旦车辆进入地库失去信号,云端通过遗嘱消息把该车标记为“离线”,恢复连接后自动补发升级包。

能源监控数据管道

阶段:数据汇聚与流式分析

解决的问题:分布式光伏电站的设备分散在野外,网络不稳定,传统采集网关成本高。MQTT 提供低带宽接入和离线缓存,Kafka 负责下游大规模流式计算。

具体案例:某能源集团在 5000 个光伏逆变器上部署 MQTT 客户端,每 5 秒上报功率和温度,Broker 集群转发到 Kafka,AI 系统实时比对发电效率衰减,自动识别异常组件并派单检修。

Python 演示

python109 行
# -*- coding: utf-8 -*-"""极简 MQTT 模拟器:演示主题匹配、保留消息和遗嘱。"""  class MiniBroker:    def __init__(self):        self.subscribers = {}        self.sessions = {}        self.retained = {}        self.gc_count = 0     def register(self, client):        self.sessions[client.cid] = client     def subscribe(self, cid, topic_filter):        self.subscribers.setdefault(topic_filter, set()).add(cid)        for topic, msg in self.retained.items():            if self._topic_matches(topic_filter, topic):                self.sessions[cid].deliver(topic, msg['payload'], msg['qos'])     def unsubscribe(self, cid, topic_filter):        self.subscribers.get(topic_filter, set()).discard(cid)     def match(self, topic):        found = set()        for topic_filter, clients in self.subscribers.items():            if self._topic_matches(topic_filter, topic):                found |= clients        return found     def _topic_matches(self, topic_filter, topic):        f_parts = topic_filter.split('/')        t_parts = topic.split('/')        i = 0        for f in f_parts:            if f == '#':                return True            if i >= len(t_parts):                return False            if f != '+' and f != t_parts[i]:                return False            i += 1        return i == len(t_parts)     def publish(self, topic, payload, qos=0, retain=False):        if retain:            self.retained[topic] = {'payload': payload, 'qos': qos}        for cid in self.match(topic):            self.sessions[cid].deliver(topic, payload, qos)     def send_will(self, client):        if client.will:            self.publish(*client.will)     def disconnect(self, client, abnormal=True):        if abnormal:            self.send_will(client)        else:            self.sessions.pop(client.cid, None)  class MiniClient:    def __init__(self, cid, broker, will=None):        self.cid = cid        self.broker = broker        self.will = will        self.inbox = []        self.pending_pubrel = set()        broker.register(self)     def subscribe(self, topic_filter):        self.broker.subscribe(self.cid, topic_filter)     def publish(self, topic, payload, qos=0, retain=False):        if qos == 2:            self.pending_pubrel.add(topic)        self.broker.publish(topic, payload, qos, retain)     def complete_qos2(self, topic):        self.pending_pubrel.discard(topic)     def deliver(self, topic, payload, qos):        self.inbox.append((topic, payload, qos))  def main():    broker = MiniBroker()    sensor = MiniClient('sensor-01', broker)    dashboard = MiniClient('dashboard', broker)    dashboard.subscribe('factory/#')    dashboard.subscribe('alert/#')    sensor.publish('factory/temp', '36.5', qos=2, retain=True)    print('dashboard after publish:', dashboard.inbox)    print('retained:', broker.retained)     late = MiniClient('late-viewer', broker)    late.subscribe('factory/#')    print('late viewer receives retained:', late.inbox)     watchdog = MiniClient('watchdog', broker, will=('alert/offline', 'sensor-01', 1, False))    broker.disconnect(watchdog, abnormal=True)    print('after will:', dashboard.inbox)    print('sensor pending QoS2:', sensor.pending_pubrel)    sensor.complete_qos2('factory/temp')    print('after QoS2 complete:', sensor.pending_pubrel)  if __name__ == '__main__':    main()
目的:用纯 Python 模拟一个迷你 MQTT Broker,展示主题通配符匹配、保留消息投递给新订阅者、遗嘱消息触发和 QoS2 状态清理。
  1. dashboard.subscribe('factory/#')订阅所有以 factory/ 开头的主题,井号通配符会匹配剩余所有层级。
  2. sensor.publish(... qos=2, retain=True)以 QoS 2 发布一条温度数据,同时把这条消息存为保留消息。
  3. if self._topic_matches(topic_filter, topic)核心的主题匹配函数,逐个层级比较 + 和 # 通配符。
  4. if retain: self.retained[topic] = ...Broker 把 retain 消息保存到字典,相当于邮局的公告板。
  5. for topic, msg in self.retained.items()新订阅者注册时,立即收到所有匹配的保留消息。
  6. if client.will: self.publish(*client.will)客户端异常断开时,Broker 代发遗嘱消息。
  7. self.pending_pubrel.add(topic)记录 QoS 2 发布中待完成的 Topic,模拟服务端去重状态。

常见误区

误区我以前觉得 MQTT 也像 Kafka 一样把消息都存在硬盘里,可以随便回放,选型时只要用 MQTT 就能搞定所有场景。

正解MQTT 是发布/订阅协议,Broker 不是以“队列”为核心存储模型。它默认只做路由和转发,内存中保留的消息非常有限(通常只有 retained 消息和离线会话内的 QoS1/2 消息),不能像 Kafka 那样按 offset 长期重放历史数据。它更像蜂鸟邮局,送完信就放下,不负责保存城市档案馆。

为何常见:因为“消息队列选型”这个分类里常把消息中间件并列介绍,新手容易把所有“消息系统”都当成一回事。

误区为了保证消息不丢,我给所有传感器都配 QoS 2,结果设备上报延迟高、重传频繁,系统反而更不稳定。

正解QoS 2 需要四步握手,在弱网下重传会占满带宽,也可能导致消息堆积。QoS 0 适合高频遥测,QoS 1 适合指令下发,QoS 2 只适合对重复和丢失都敏感的极端场景,比如计费、资金类消息。可靠性的背后是成本,蜂鸟面签虽然稳妥,但每封信都面签,邮局早就瘫痪了。

为何常见:人脑天然偏好“更可靠”,忽略了协议开销与业务风险之间的平衡。

误区我以为把 Clean Session 设成 0,Broker 就会一直保存所有历史消息,像数据库一样想查就查。

正解Clean Session=0 只是让 Broker 保存“会话状态”,包括订阅关系和离线期间未确认的 QoS1/2 消息,并不是全量消息存储。如果设备离线太久,超过 Session Expiry Interval,会话一样会过期清理。持久化存储还是得交给 Kafka 或数据库。

为何常见:因为“持久会话”这个名字容易让人误解为“持久存储”。

误区我可以在业务代码里随时 send 一条遗嘱消息,等设备断电前主动发出去。

正解遗嘱消息是在建立连接时通过 CONNECT 报文预先登记给 Broker 的,客户端自己无法在运行中“临时发送”遗嘱。只有异常断开(网络中断、崩溃)时 Broker 才会代发;正常断开时遗嘱会被撤销。它本质上是一份提前寄放在邮局的“意外预案”,不是业务流程里的普通消息。

为何常见:新手容易先理解“Will”字面意思,忽略它在协议层的特殊触发条件。

面试题

Q1. MQTT 默认运行在下面哪个传输层协议之上?

单选题 · easy

  • A.TCP 协议← 正确答案
  • B.UDP 协议
  • C.HTTP 协议
  • D.FTP 协议

答案:A。MQTT 设计之初就选择 TCP 作为底层传输,因为它要求可靠、有序、面向连接。UDP 虽然更轻,但无法保证消息顺序;HTTP/FTP 是应用层协议,不是传输层。可以把 TCP 理解成蜂鸟邮局的稳定跑道,UDP 更像随风飞的纸飞机。

Q2. 主题过滤器 a/+/c 可以匹配下面哪个主题?

单选题 · easy

  • A.a/b/c← 正确答案
  • B.a/b/d
  • C.x/a/c
  • D.a/c/x

答案:A。+ 通配符只匹配一层,所以它只能替换中间一层,最终层级必须是 c。A 的三段分别是 a、b、c,完全匹配;B 最后一段是 d,C 第一段是 x,D 最后一段是 x,都不符合。就像门牌“李府/+/厨房”只能找到“李府/任意屋/厨房”。

Q3. QoS 2 完成一次可靠投递最少需要几条报文?

单选题 · easy

  • A.1条
  • B.2条
  • C.4条← 正确答案
  • D.8条

答案:C。QoS 2 使用 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步握手,保证不重不丢。QoS 0 只需要 1 条,QoS 1 需要 2 条。四步握手就像面签寄信:发件人提出、收件人确认、发件人再确认收件人的确认、收件人最终签收。

Q4. 关于 MQTT 保留消息,下列说法正确的是?

单选题 · mid

  • A.新订阅者会立即收到保留消息← 正确答案
  • B.保留消息会永久保存所有版本
  • C.保留消息只会发给发布者本人
  • D.保留消息在每次发布后自动清零

答案:A。保留消息是 Broker 为每个主题保存的“最新一条”,新订阅者订阅时会立刻收到,类似公告板上的最新告示。它不会保存所有历史版本,也不是只发给发布者;只有发布新消息并设置 retain 才会覆盖旧值。

Q5. 服务端判定客户端离线的时间阈值通常是 KeepAlive 的几倍?

单选题 · mid

  • A.0.5 倍
  • B.1.0 倍
  • C.1.5 倍← 正确答案
  • D.3.0 倍

答案:C。MQTT 协议规定,如果服务端在 1.5 倍 KeepAlive 时间内没有收到客户端任何报文,则判定连接已断开。这个 1.5 倍是为了容忍网络抖动,就像蜂鸟允许客户迟到半小时,但不会一直等下去。

Q6. 下列哪些场景更适合选择 MQTT 作为接入协议?

多选题 · mid

  • A.低带宽传感器数据上报← 正确答案
  • B.海量日志离线批量分析
  • C.弱网环境下的指令下发← 正确答案
  • D.数据库跨表强一致事务

答案:AC。MQTT 的轻量报头和 QoS 机制天然适合低带宽传感器和弱网指令下发;海量日志离线分析更适合 Kafka,数据库强一致事务应该用专门的事务消息方案。选 MQTT 就像选蜂鸟送小纸条,而不是开着拖拉机去送快递。

Q7. 若按 \(t_n = t_{base} \times 2^n\) 计算(n 为连续失败次数),连续失败第 10 次后的重试延迟约为多少?

单选题 · hard

  • A.512 秒
  • B.1024 秒← 正确答案
  • C.8192 秒
  • D.2048 秒

答案:B。当 n=10 时,\(2^{10}=1024\),所以延迟约为基准值的 1024 倍。若基准为 1 秒,就是约 1024 秒。指数退避让重试时间快速增长,避免所有设备同时冲击 Broker,像蜂鸟在高峰期错峰起飞而不是一窝蜂乱撞。

Q8. 订阅过滤器 sensors/# 能否匹配 sensors 这一主题?

单选题 · hard

  • A.能,井号包含父级← 正确答案
  • B.不能,只能匹配子级
  • C.能,但仅限于在线客户端
  • D.不能,必须写成 sensors/+

答案:A。在 MQTT 中,# 通配符会匹配父级及所有后续层级,所以 sensors/# 能匹配 sensors 本身,也能匹配 sensors/temp。如果只匹配一层且必须非空,才用 +。订阅过滤器就像邮局的分拣规则,井号表示“这一柜及以下所有隔间都归我管”。

Q9. 以下哪些是选择 MQTT 而非 Kafka 的合理理由?

多选题 · hard

  • A.协议字节开销更低← 正确答案
  • B.天然支持海量分区
  • C.更适合弱网设备接入← 正确答案
  • D.支持离线消息无限重放

答案:AC。MQTT 固定报头小、支持 QoS 和遗嘱,非常适合弱网设备;Kafka 则以高吞吐、分区和日志回放见长。海量分区和离线无限重放更贴近 Kafka 的强项。MQTT 更像是为“现场接入”而生的轻骑,Kafka 是为“中心存储”而建的大仓库。

Q10. 关于 MQTT 会话续存(Clean Session=0),下列说法正确的是?

多选题 · extreme

  • A.服务端会保存订阅关系← 正确答案
  • B.离线期间 QoS1/2 消息可补投← 正确答案
  • C.会话可设置过期时间← 正确答案
  • D.心跳断开后立即删除会话

答案:ABC。Clean Session=0 表示持久会话,Broker 保存订阅关系和离线消息,也可配合 Session Expiry Interval 设置过期时间;设备重连后重新投递未确认的 QoS1/2 消息。心跳断开不一定立刻删除会话,只有清理会话标志或会话过期才会删除。这就像蜂鸟邮局保留客户的储物柜,而不是客户出门就立刻砸柜。