MQTT 发布订阅是怎么工作的?主题、通配符和 Broker 怎么配合?
MQTT 的发布/订阅模式可以理解成“发消息的人不找具体接收者,只把消息交给主题;想要消息的人订阅主题”。发布者只负责把 payload 发到 topic,订阅者只声明自己关心哪些 topic,中间的 Broker 负责匹配和分发。这个设计把生产者和消费者解耦了,所以很适合设备多、上下线频繁、消息一对多的物联网场景。
一条消息是怎么走的?
流程并不复杂。订阅者先连接 Broker,然后发送 SUBSCRIBE,例如订阅 home/+/temperature。传感器作为发布者把温度发到 home/livingroom/temperature,Broker 发现这个主题匹配订阅规则,就把消息推给订阅者。发布者并不知道谁收到了消息,订阅者也不需要知道消息来自哪台设备,双方只通过 topic 间接关联。
Topic 是 MQTT 路由的核心。它是用斜杠分隔的层级字符串,比如 tenant/a/device/001/status。主题区分大小写,Home 和 home 是两个主题。设计主题时不要只考虑今天的功能,还要考虑权限、统计、扩展和排障,否则后面 ACL 和数据分析都会很难做。
通配符让订阅变得灵活。+ 匹配单层,例如 home/+/temperature 可以匹配客厅和卧室温度。# 匹配多层,但只能放在末尾,例如 home/# 能收到 home 下所有消息。通配符很方便,也很危险,生产环境要避免业务客户端随便订阅大范围主题。
发布订阅不是消息队列的简单替代品。MQTT 更强调实时推送和连接管理,消息是否离线保存取决于会话、QoS 和 Broker 配置。多个订阅者订阅同一主题时,默认每个订阅者都会收到一份消息;如果想做负载均衡,需要使用共享订阅,例如 MQTT 5 常见的 $share/group/sensor/#。
真实项目里还要区分“状态”和“事件”。状态可以用 retained message 保留最后一条,比如设备在线状态;事件则应该进入后端存储,比如告警流水和操作记录。把两者混在一起会出问题:新客户端上线后拿到一条 retained 告警,可能误以为刚刚发生。主题命名和 payload 里最好明确消息类型。
bashmosquitto_sub -h localhost -t 'home/+/temperature' mosquitto_pub -h localhost -t 'home/livingroom/temperature' -m '25.6' mosquitto_pub -h localhost -t 'home/kitchen/humidity' -m '60%'
发布订阅还有一个好处是便于旁路扩展。原来只有监控服务订阅设备状态,后来新增告警、数据清洗或调试工具,只要再订阅同一类主题即可,不需要改发布端代码。不过这也带来治理问题:谁订阅了什么、是否还在消费、是否造成重复处理,都需要有可观测性。Broker 侧的订阅列表和消费延迟应纳入日常排查。
追问
发布者和订阅者真的完全不知道彼此吗?
协议层面是解耦的,发布者不需要保存订阅者列表,订阅者也不直接连接发布者。业务层面通常还是会约定 payload 格式、主题命名和设备身份,否则收到消息也不知道怎么处理。取舍在于灵活性和治理成本:解耦让扩展容易,但主题规范一旦缺失,系统会变成没人敢改的消息网。
主题应该怎么设计才不容易后悔?
建议把租户、产品、设备和方向放进主题,例如 tenant/{tid}/product/{pid}/device/{id}/up。这样 ACL 可以按路径限制,日志也容易按设备定位。不要把大量业务字段塞进 topic,比如温度值、时间戳应该放 payload,不该放主题。边界是 topic 适合做路由维度,不适合承载所有数据维度。
+ 和 # 通配符有什么坑?
+ 只匹配一层,# 匹配多层并且只能出现在末尾,这两个规则经常被写错。订阅 home/# 会收到 home 下几乎所有消息,调试时方便,生产里可能造成流量暴涨。还有一个坑是 ACL 放开了通配符订阅,普通设备就可能读到别人的数据。通配符应该更多给平台服务用,终端设备尽量订阅精确主题。
发布订阅和点对点消息有什么区别?
点对点模式通常知道明确接收者,消息只交给一个目标。MQTT 发布订阅默认是一对多,任何匹配订阅的客户端都能收到消息。它适合状态广播、设备上报、告警通知,不适合需要严格单消费者处理的任务队列。需要负载均衡消费时,可以用共享订阅,但仍要处理重复投递和消费幂等。
为什么订阅后才收到消息?以前的消息去哪了?
普通订阅只接收订阅建立之后的新消息,之前发布的消息不会自动补发。想让新客户端上线就拿到最近状态,可以用 Retained Message;想让离线客户端恢复后收到消息,要使用持久会话和合适的 QoS。踩坑点是把 retained 当历史消息,它只保留每个主题最后一条。真正的历史查询应该从数据库查,而不是指望 Broker 保存全部消息。