物联网实战|Spring Boot + Netty 搭建 MQTT 消息路由与流转层

物联网实战|Spring Boot + Netty 搭建 MQTT 消息路由与流转层

源码(sample-00-iot-routing)

https://gitee.com/kcnf-iot/mqtt-boot/tree/master/sample-00

sample-00-iot-routing 物联网消息路由模块

模块概述

sample-00-iot-routing 是物联网平台的核心消息路由模块,负责维护设备订阅关系与消息分发。该模块处于接入层与业务层之间,接收来自接入层的上行消息事件,根据订阅关系匹配目标设备,通过 Netty Channel 将消息转发给在线订阅者。

模块职责

维护主题订阅关系,支持 MQTT 通配符 +# 规则匹配 管理订阅关系存储,提供内存与 Redis 双模式实现 处理上行消息分发,根据主题查找订阅者并转发消息 处理下行消息发送,根据协议类型封装不同格式消息 记录订阅操作日志,支持订阅行为审计

模块结构

复制代码
消息事件源(接入层)
    │
    ▼
DeviceSubscribeEventListener ───► SubscriptionManager
    │                              ├── 内存模式 (Inner)
    │                              └── Redis模式 (Redis + Caffeine)
    │
    ▼
TopicMatcher (Trie树实现)
    │
    ▼
MessageEventListener(异步处理)
    │
    ├── 主题匹配查找订阅者
    │
    ▼
MessageSendDispatcher
    │
    ├── MQTT 协议格式封装
    │
    └── TCP 协议格式封装
    │
    ▼
RegManager (获取设备 Channel)
    │
    ▼
Netty Channel.writeAndFlush() ───► 目标设备

目录结构

复制代码
sample-00-iot-routing/
├── src/main/java/com/jysemel/iot/routing/
│   ├── config/
│   │   └── RoutingTraceIdTaskExecutorAsyncConfig.java  # 异步线程池配置,传递 TraceId
│   ├── dispatcher/
│   │   └── MessageSendDispatcher.java                  # 下行消息分发器,支持多协议发送
│   ├── listener/
│   │   ├── DeviceSubscribeEventListener.java           # 设备订阅事件监听器
│   │   └── MessageEventListener.java                   # 消息事件监听器,处理消息分发
│   ├── matcher/
│   │   ├── TopicMatcher.java                           # 主题匹配器接口
│   │   └── TrieTopicMatcher.java                       # Trie树实现的主题匹配器
│   ├── subscription/
│   │   ├── Subscription.java                           # 订阅实体
│   │   ├── SubscriptionManager.java                    # 订阅管理器接口
│   │   └── impl/
│   │       ├── InnerMemerySubscriptionManager.java     # 内存模式订阅管理
│   │       └── RedisSubscriptionManager.java           # Redis模式订阅管理
│   └── subscriptionlog/
│       ├── SubscriptionLogDO.java                      # 订阅日志实体
│       ├── SubscriptionLogRepository.java              # 订阅日志仓储接口
│       ├── SubscriptionLogService.java                 # 订阅日志服务接口
│       └── impl/
│           ├── MySqlSubscriptionLogService.java        # MySQL订阅日志实现
│           └── SqliteSubscriptionLogService.java       # SQLite订阅日志实现
├── README.md
└── pom.xml

关键实体

Subscription 订阅实体

表示一条订阅关系,包含以下字段:

字段 类型 说明
clientId String 客户端唯一标识
topic String 订阅主题,可包含通配符
qos int 服务质量等级 (0, 1, 2)
subscribeTime long 订阅时间戳,毫秒级
deviceId String 设备ID,可选字段

SubscriptionLogDO 订阅日志实体

记录订阅操作的审计日志,包含以下字段:

字段 类型 说明
id Long 主键,自增
clientId String 客户端标识,长度限制 128
topic String 订阅主题,长度限制 256
qos Integer 服务质量等级
action String 操作类型,可选 SUBSCRIBE / UNSUBSCRIBE / UNSUBSCRIBE_ALL
protocol String 协议类型,如 MQTT / TCP
actionTime LocalDateTime 操作时间
recordTime LocalDateTime 记录时间,自动生成
remark String 备注信息,长度限制 255

主题匹配机制

TopicMatcher 接口

主题匹配器提供核心能力:主题过滤器与发布主题的匹配、按主题查找订阅者、管理订阅关系。

提供的主要方法:

  • match(String topicFilter, String topic) - 判断订阅过滤器与主题是否匹配
  • findMatches(String topic) - 查找匹配指定主题的所有订阅过滤器
  • addSubscription(String topicFilter, String subscriber) - 添加订阅关系
  • removeSubscription(String topicFilter, String subscriber) - 移除订阅关系
  • clear() - 清空所有订阅关系

TrieTopicMatcher Trie树实现

基于前缀树(Trie树)实现的主题匹配器,支持 MQTT 标准通配符规则。

通配符规则说明:

  • 单级通配符 +:匹配主题中的一个层级

    • sensor/+/temperature 可匹配 sensor/device1/temperature
    • 不可匹配 sensor/device1/room/temperature
  • 多级通配符 #:匹配主题中的剩余所有层级

    • sensor/# 可匹配 sensor/device1/temperature
    • 可匹配 sensor/device1/room/temperature/humidity
    • 必须位于过滤器末尾

内部数据结构:

使用 TrieNode 节点构建主题层级树,每个节点维护子节点映射与订阅者列表。

复制代码
TrieNode
├── children: Map<String, TrieNode>
│   ├── "sensor"  → TrieNode
│   ├── "+"       → TrieNode (单级通配符)
│   └── "#"       → TrieNode (多级通配符)
└── subscribers: List<String> (订阅过滤器列表)

匹配流程:

消息主题 sensor/device1/temperature 查找订阅流程:

  1. / 分割主题层级:["sensor", "device1", "temperature"]
  2. 从根节点开始,逐级精确匹配
  3. 在每个层级同时尝试匹配 + 通配符节点
  4. 遇到 # 通配符节点,直接返回其订阅者列表
  5. 收集所有命中的订阅者返回

订阅管理

SubscriptionManager 接口

订阅管理核心接口,定义订阅生命周期管理方法:

  • subscribe(String clientId, String topic, int qos) - 添加订阅
  • unsubscribe(String clientId, String topic) - 移除指定主题订阅
  • unsubscribeAll(String clientId) - 移除客户端所有订阅
  • getSubscriptions(String clientId) - 获取客户端所有订阅
  • getSubscribers(String topic) - 获取主题的所有订阅者
  • findMatchingSubscriptions(String topic) - 查找匹配主题的所有订阅(含通配符)
  • isSubscribed(String clientId, String topic) - 检查是否已订阅

InnerMemerySubscriptionManager 内存实现

基于 ConcurrentHashMapCopyOnWriteArrayList 实现的本地内存订阅管理器,适用于单机部署场景。

启用条件:

复制代码
mqtt.sub=inner

或不配置 mqtt.sub 参数(默认启用)。

数据存储结构:

  • clientSubscriptions: Map<String, List<Subscription>>
    • Key: clientId
    • Value: 客户端订阅列表
  • topicMatcher: TopicMatcher 实例,维护主题过滤器索引

主要流程:

subscribe 方法流程:

  1. 检查客户端是否已订阅相同主题(去重)
  2. 如未订阅,创建 Subscription 对象并添加到客户端列表
  3. 将主题过滤器添加到 TopicMatcher 索引中
  4. 记录日志

unsubscribe 方法流程:

  1. 从客户端订阅列表中移除指定主题
  2. 从 TopicMatcher 索引中移除订阅关系
  3. 记录日志

findMatchingSubscriptions 方法流程:

  1. 调用 TopicMatcher.findMatches() 获取匹配的主题过滤器
  2. 对每个过滤器,获取对应的订阅者列表
  3. 聚合返回结果

特点:

  • 高性能,纯内存操作,响应快
  • 无需外部依赖(Redis)
  • 单机模式下数据不持久化,重启后丢失
  • 适合开发测试环境或单点部署

RedisSubscriptionManager Redis 实现

基于 Redis 与 Caffeine 本地缓存的分布式订阅管理器,适用于多节点集群部署场景。

启用条件:

复制代码
mqtt.sub=redis

数据存储结构:

Redis 数据结构:

  • 设备订阅列表 (List): key = iot:subscription:device:{clientId}

    • 每个元素为 JSON 序列化的 Subscription 对象
    • 过期时间 30 天
  • 主题索引 (Set): key = iot:subscription:topic:{topic}

    • 每个元素为订阅该主题的 clientId
    • 用于快速查找主题订阅者

Caffeine 本地缓存:

  • localCache: Cache<String, List<Subscription>>

    • Key: clientId
    • Value: 客户端订阅列表
    • 最大 10000 条,写入后 10 分钟过期
  • topicCache: Cache<String, List<Subscription>>

    • Key: topic
    • Value: 主题订阅者列表
    • 最大 10000 条,写入后 10 分钟过期

主要流程:

subscribe 方法流程:

  1. 检查是否已订阅(从缓存或 Redis)
  2. 未订阅则序列化 Subscription 对象并存入 Redis List
  3. 将 clientId 添加到主题索引 Set
  4. 清除本地缓存(使缓存失效)
  5. 将主题过滤器添加到 TopicMatcher 索引
  6. 记录日志

getSubscriptions 方法流程(缓存优先):

  1. 先从 localCache 读取
  2. 缓存命中则直接返回
  3. 未命中则从 Redis 读取 List 并反序列化
  4. 更新本地缓存后返回

getSubscribers 方法流程(缓存优先):

  1. 先从 topicCache 读取
  2. 缓存命中则直接返回
  3. 未命中则从 Redis 主题索引 Set 获取 clientId 列表
  4. 对每个 clientId,获取其订阅并过滤当前主题
  5. 聚合后更新本地缓存并返回

特点:

  • 支持多节点分布式部署
  • 数据持久化在 Redis,重启不丢失
  • 通过 Caffeine 本地缓存降低 Redis 压力
  • 写入后主动清除缓存,保证数据一致性
  • 适合生产环境集群部署

消息分发

MessageEventListener 消息事件监听器

监听 Spring 事件总线的 MessageEvent 事件,在异步线程池中处理消息分发。

主要依赖:

  • subscriptionManager - 订阅管理器,用于查找订阅者
  • messageSendDispatcher - 消息发送分发器,实际发送消息
  • eventPublisher - 事件发布器,发布规则引擎事件

事件处理流程:

handleMessageUpstream 方法流程:

  1. 监听 Spring @EventListener 注解捕获 MessageEvent
  2. 通过 @Async("routingTraceIdTaskExecutor") 切换到异步线程池执行
  3. 从事件中提取 InternalMessage
  4. 检查消息主题,如果为空则自动生成默认主题 /mock/{clientId}/data
  5. 调用 dispatch 方法执行分发逻辑

dispatch 方法流程:

  1. 提取消息主题
  2. 调用 subscriptionManager.findMatchingSubscriptions(topic) 查找匹配的订阅
  3. 无订阅者则直接返回,记录日志
  4. 有订阅者则遍历每个订阅,创建 MessageDispatchEvent
  5. 调用 publishEvent 执行实际发送

publishEvent 方法流程:

  1. 调用 messageSendDispatcher.send(targetClientId, originalMessage) 发送消息
  2. 捕获 DeviceOfflineException,记录 WARN 日志(设备离线为正常场景)
  3. 捕获通用 Exception,记录 ERROR 日志
  4. 发布 RuleEvent 事件给规则引擎后续处理
  5. 捕获规则引擎处理异常并记录日志

日志级别策略:

  • 设备离线使用 WARN 级别,属于业务正常情况
  • 发送异常使用 ERROR 级别,属于系统异常情况

MessageSendDispatcher 消息发送分发器

下行消息发送核心组件,根据协议类型将 InternalMessage 封装为对应协议格式后发送到目标设备。

主要依赖:

  • regManager - 设备注册管理器,用于根据 clientId 查找 Netty Channel

send 方法主流程:

  1. 通过 regManager.getConnection(clientId) 获取设备 Channel
  2. Channel 为空说明设备离线,抛出 DeviceOfflineException
  3. Channel 不为空但 isActive() 为 false,说明连接已断开
    • 清理无效连接记录
    • 抛出 DeviceOfflineException
  4. 读取协议类型,默认为 TCP
  5. 根据协议类型分发到具体发送方法
  6. 捕获发送异常,关闭异常连接并清理记录
  7. 抛出 RuntimeException 供上层处理

sendTcpMessage TCP 发送流程:

  1. 创建 ByteBuf,预留 payload 长度 + 1 字节
  2. 写入 payload 字节数组
  3. 写入换行符 \n 作为消息结束标识
  4. 调用 channel.writeAndFlush(byteBuf) 发送
  5. 记录发送日志,包含完整报文信息

sendMqttPublish MQTT 发送流程:

  1. 构造 MqttFixedHeader

    • 消息类型:PUBLISH
    • QoS 等级:从 message.getQos() 获取
    • Retain 标志:从 message.isRetained() 获取
    • RemainingLength: 0 (Netty 自动计算)
  2. 构造 MqttPublishVariableHeader

    • Topic: 消息主题
    • PacketId: QoS > 0 时生成 (System.currentTimeMillis() % 65535)
    • PacketId: QoS = 0 时为 0
  3. 构造消息体 ByteBuf

    • 创建 payload 长度的缓冲区
    • 写入 payload 字节数组
  4. 组装 MqttPublishMessage 并通过 Netty Channel 发送

  5. 记录发送日志

异常处理策略:

  • 发送过程中发生任何异常,记录 ERROR 日志
  • 如果 Channel 仍处于 active 状态,主动关闭
  • 清理 RegManager 中的连接记录
  • 抛出 RuntimeException 供上层业务处理

订阅事件处理

DeviceSubscribeEventListener 设备订阅事件监听器

监听设备订阅/取消订阅事件,维护订阅关系并记录操作日志。

事件源:接入层通过 Spring 事件总线发布 DeviceSubscribeEvent

主要依赖:

  • subscriptionManager - 订阅管理器,可选注入(required=false)
  • subscriptionLogService - 订阅日志服务,可选注入(required=false)

handleDeviceSubscribe 方法流程:

  1. 检查 subscriptionManager 是否注入,未注入则记录 WARN 日志并跳过

  2. 获取事件信息:clientId、事件类型、协议类型(默认 MQTT)

  3. 根据事件类型 switch 处理:

    • SUBSCRIBE 类型:调用 subscriptionManager.subscribe() 添加订阅,记录操作日志
    • UNSUBSCRIBE 类型:调用 subscriptionManager.unsubscribe() 移除订阅,记录操作日志
    • UNSUBSCRIBE_ALL 类型:调用 subscriptionManager.unsubscribeAll() 移除全部订阅,记录操作日志
  4. 捕获通用异常,记录 ERROR 日志

异步处理:通过 @Async("routingTraceIdTaskExecutor") 在独立线程池中执行,避免阻塞主线程。

订阅日志服务

SubscriptionLogService 接口

定义订阅操作日志记录能力:

  • logSubscribe(String clientId, String topic, Integer qos, String protocol) - 记录订阅操作
  • logUnsubscribe(String clientId, String topic, String protocol) - 记录取消订阅操作
  • logUnsubscribeAll(String clientId, String protocol) - 记录取消全部订阅操作

异步线程池配置

RoutingTraceIdTaskExecutorAsyncConfig

配置路由模块专用异步线程池,支持在异步线程中传递 TraceId 以实现完整链路追踪。

线程池参数:

参数 说明
corePoolSize 4 核心线程数
maxPoolSize 8 最大线程数
queueCapacity 500 任务队列容量
threadNamePrefix "async-multi-" 线程名前缀

TraceId 传递机制:

通过 TaskDecorator 在切换线程时复制 MDC (Mapped Diagnostic Context) 上下文:

  1. 在提交任务前,捕获当前线程的 MDC 中指定 key 的值
  2. 在执行任务时,将捕获的值设置到新线程的 MDC 中
  3. 保存新线程的原始 MDC 值,用于任务结束后恢复
  4. 任务执行完成后,恢复新线程的原始 MDC 状态
  5. 通过 try-finally 保证无论任务执行成功与否都能正确恢复

配置的 MDC key 列表:

  • TraceIdUtil.TRACE_ID_KEY - 消息追踪 ID

通过这种机制,即使在异步线程中,也能保证日志中的 TraceId 连续性,便于问题排查。

完整消息流转示例

上行消息分发场景

设备 A 发布消息到主题 sensor/temperature/data,设备 B 和服务 C 订阅了该主题。

复制代码
设备 A ──► MQTT Broker (接入层)
              │
              ▼
         MessageEvent 发布到 Spring 事件总线
              │
              ▼
         MessageEventListener.handleMessageUpstream() [异步线程]
              │
              ├── 从消息提取 InternalMessage
              │
              ▼
         dispatch(InternalMessage)
              │
              ├── subscriptionManager.findMatchingSubscriptions(
              │     "sensor/temperature/data"
              │  )
              │
              └── 返回匹配的订阅列表: [设备B, 服务C]
              │
              ▼
         遍历订阅列表
              │
              ├── 设备B: MessageSendDispatcher.send()
              │       ├── 从 RegManager 获取设备B的 Channel
              │       ├── 协议类型为 MQTT
              │       ├── 构造 MqttPublishMessage
              │       └── channel.writeAndFlush() ► 设备 B 收到消息
              │
              └── 服务C: MessageSendDispatcher.send()
                      ├── 从 RegManager 获取服务C的 Channel
                      ├── 协议类型为 TCP
                      ├── 构造带换行符的纯文本消息
                      └── channel.writeAndFlush() ► 服务 C 收到消息
              │
              ▼
         发布 RuleEvent 给规则引擎处理

订阅管理场景

设备 B 发送 SUBSCRIBE 报文订阅主题 sensor/temperature/+

复制代码
设备 B ──► MQTT Broker (接入层)
              │
              ▼
         DeviceSubscribeEvent (SUBSCRIBE 类型) 发布
              │
              ▼
         DeviceSubscribeEventListener.handleDeviceSubscribe()
              │
              ├── 提取 clientId、topic、qos
              │
              └── subscriptionManager.subscribe(clientId, topic, qos)
                      │
                      ├── 添加到 clientSubscriptions Map
                      │
                      └── topicMatcher.addSubscription(topic, clientId)
                              │
                              └── 将 "sensor/temperature/+" 添加到 Trie 树
                      │
                      └── subscriptionLogService.logSubscribe() ► 数据库保存操作记录

模块依赖关系

pom.xml 依赖声明

复制代码
<dependency>
    <groupId>com.jysemel.iot</groupId>
    <artifactId>sample-00-iot-common</artifactId>
    <!-- 提供 InternalMessage、EventPublisher、RedisKeys、TraceIdUtil 等通用能力 -->
</dependency>

<dependency>
    <groupId>com.jysemel.iot</groupId>
    <artifactId>sample-00-iot-access</artifactId>
    <!-- 提供 RegManager 连接管理能力,用于查找设备 Channel -->
</dependency>

与其他模块的交互

与接入层的交互:

  • 接收 MessageEvent:订阅接入层发布的消息事件
  • 接收 DeviceSubscribeEvent:订阅接入层发布的设备订阅事件
  • 调用 RegManager:根据 clientId 获取设备 Channel

与公共模块的交互:

  • 使用 InternalMessage:统一消息载体
  • 使用 EventPublisher:发布 MessageDispatchEvent 和 RuleEvent
  • 使用 ModuleConstants:模块标识,用于日志标识
  • 使用 TraceIdUtil:链路追踪,保持日志关联

配置项说明

核心配置项:

复制代码
mqtt:
  sub: inner              # 订阅管理模式,inner=内存,redis=Redis
                           # 不配置时默认使用 inner 模式

使用 Redis 模式时,需要额外确保 Redis 连接配置正确,包括 host、port、password 等参数。

关键设计说明

Trie 树主题匹配

使用前缀树而非 HashMap 精确匹配,主要原因:

  • MQTT 主题支持通配符,订阅主题可能是 sensor/+/temperaturedevice/#
  • 消息发布时需要找到所有匹配的订阅过滤器
  • Trie 树按层级匹配,支持同时精确匹配和通配符匹配
  • 时间复杂度 O(n),n 为主题层级深度,性能远优于遍历所有订阅逐一正则匹配

读写并发安全

  • 订阅管理器使用 ConcurrentHashMap 存储,保证并发读写安全
  • TopicMatcher 使用 HashMap 存储子节点,CopyOnWriteArrayList 存储订阅者
  • 写操作较少,订阅/取消订阅的频率远低于消息匹配频率
  • CopyOnWriteArrayList 在读多写少场景下提供优秀性能

Caffeine 本地缓存

Redis 模式下,使用 Caffeine 作为本地缓存层:

  • 降低 Redis 网络请求频率,减少延迟
  • 缓存有效期 10 分钟,保证最终一致性
  • 写操作后主动清除缓存,避免读取脏数据
  • maximumSize 限制缓存最大条目,防止内存溢出

异步线程与 TraceId 传递

消息分发使用独立线程池异步处理:

  • 不阻塞接入层主线程,提高消息接收吞吐量
  • 通过 TaskDecorator 复制 MDC 上下文,保持日志链路可追踪
  • TraceId 从消息接收开始贯穿到分发结束,统一日志查询

多协议消息发送

MessageSendDispatcher 根据协议类型选择发送方式:

  • MQTT 协议:封装为 MqttPublishMessage,支持 QoS、Retain 等 MQTT 特性
  • TCP 协议:直接发送字节数组,添加换行符作为消息边界,便于客户端解析
  • 设计预留扩展点,未来添加 CoAP / WebSocket 等协议支持

监控与日志

核心日志标识:

  • ModuleConstants.MODULE_ROUTING:统一前缀 "routing" 或对应模块名,便于日志过滤
  • TraceId:每条链路请求的唯一标识,贯穿消息接收、分发、转发全流程

关键日志场景:

  • 订阅成功:记录 clientId、topic、qos,便于排查订阅关系
  • 订阅去重:记录 WARN,发现重复订阅时拦截
  • 分发统计:记录主题匹配到的订阅者数量,监控消息分发负载
  • 设备离线:记录 WARN,设备离线为正常业务场景
  • 发送失败:记录 ERROR,包含异常类型和目标 clientId
  • Trie 树操作:记录 DEBUG 级别详细日志,开发调试用

使用指南

在 Spring Boot 应用中引入本模块:

  1. 在 pom.xml 中添加依赖:

    com.jysemel.iot sample-00-iot-routing
  2. 在 application.yml 中配置订阅管理模式(可选,默认 inner):

    mqtt:
    sub: inner # 或 redis

  3. 确保事件总线可用(Spring 默认事件机制,无需额外配置)

  4. 消息发送示例(业务层):

    // 构造 InternalMessage
    InternalMessage message = new InternalMessage();
    message.setTopic("command/device001");
    message.setPayload("turn on".getBytes());
    message.setQos(1);
    message.setProtocol("MQTT");

    // 通过 MessageSendDispatcher 发送
    messageSendDispatcher.send("device001", message);

扩展建议

增加订阅持久化

当前 Redis 模式下订阅数据存在过期时间(30 天),建议增加:

  • 持久化存储层,如 MySQL 存储历史订阅
  • 订阅恢复机制,服务重启后自动重建订阅索引

消息质量保障

当前 QoS > 0 的消息没有重传机制,建议增加:

  • 未确认消息存储(In-Flight 队列)
  • 定期重发未确认消息
  • 重发次数限制与丢弃策略

限流与熔断

在分发器层面增加:

  • 按 clientId 级别限流,防止某个订阅者消息过多影响其他
  • 熔断机制,当某个订阅者发送失败次数达到阈值时暂停发送

订阅审计

增强订阅日志功能:

  • 记录订阅者 IP 地址
  • 记录订阅来源(客户端主动订阅/管理平台强制订阅)
  • 统计订阅主题分布,用于热点分析
  • 订阅变更历史可追溯

性能优化建议

Trie 树节点优化

当前 Trie 节点的 children 使用 HashMap,可考虑:

  • 对高频率前缀使用数组直接索引,如 $SYS 系统主题
  • 对低频率前缀保持 HashMap,减少内存占用
  • 增加节点合并逻辑,减少空节点

订阅查找缓存

findMatchingSubscriptions 调用频率较高,可优化:

  • 对热门主题的查找结果增加短期缓存(几秒)
  • 使用订阅变更事件触发缓存失效
  • 降低 TopicMatcher 中递归查找的频率

批量消息分发

当前消息逐条发送,可考虑:

  • 批量收集发送任务,按 Channel 聚合后批量写入
  • 减少 Netty writeAndFlush 调用次数,提升 IO 效率
  • 批量操作日志聚合输出

故障排查指南

问题:订阅后收不到消息

排查步骤:

  1. 检查 DeviceSubscribeEventListener 是否收到订阅事件
  2. 查看 subscriptionManager.isSubscribed() 返回值是否为 true
  3. 检查 TopicMatcher 中是否包含订阅过滤器
  4. 查看消息发布时 findMatchingSubscriptions 是否返回订阅者
  5. 确认设备 Channel 是否在线(通过 RegManager.getConnection())
  6. 检查 Netty 发送日志,确认消息是否写入 Channel

问题:消息发送失败但设备在线

排查步骤:

  1. 检查 MessageSendDispatcher 中的异常日志
  2. 确认 channel.isActive() 是否为 true
  3. 检查 Netty Pipeline 是否正确配置编码器
  4. 检查 payload 是否为空或格式异常
  5. 检查 QoS 级别与设备能力是否匹配
  6. 查看 TCP 抓包确认消息是否发送到网络

问题:Redis 模式下订阅丢失

排查步骤:

  1. 确认 Redis 服务是否正常运行
  2. 检查 RedisKey 是否正确(是否有命名空间隔离)
  3. 检查 Redis 过期时间是否被误设置为 0
  4. 确认本地缓存与 Redis 数据是否一致
  5. 查看是否有其他节点同时写入导致数据覆盖

问题:TraceId 不传递到异步线程

排查步骤:

  1. 确认 RoutingTraceIdTaskExecutorAsyncConfig 是否被 Spring 扫描
  2. 确认 @Async 注解是否正确指定了 bean 名称
  3. 检查 MDC_KEYS 常量中是否包含正确的 key
  4. 确认提交任务前 MDC 中已设置 TraceId
  5. 检查其他线程池是否干扰了 MDC 上下文

总结

sample-00-iot-routing 模块作为物联网平台的消息路由中枢,承担订阅关系维护与消息分发的核心职责。通过 Trie 树主题匹配、多模式订阅存储、多协议消息封装、异步线程池处理等设计,实现了高性能、可扩展、易维护的消息路由能力。

该模块与接入层松耦合,通过 Spring 事件机制交互,不依赖具体协议实现,可独立扩展新协议支持,为上层业务层提供统一的消息收发接口。

相关推荐
黄毛火烧雪下1 小时前
Java 基础笔记:文件、递归与字符编码
java·开发语言·笔记
学计算机的计算基1 小时前
链表算法上篇:LeetCode 206/234/141/142/160/21 题解与易错点
java·笔记·算法·链表
信也科技布道师1 小时前
从Istio 503 NC 错误深入理解 Mesh 路由全链路原理
java·服务器·网络
swordbob1 小时前
3 大 I/O 模型BIO / NIO / AIO
java·linux·spring
Pluto_CSND1 小时前
Cron表达式使用说明
java
十五喵源码网1 小时前
基于SpringBoot2+vue2的酒店客房管理系统
java·毕业设计·springboot·论文笔记
swordbob2 小时前
CAP 定理:为什么不能同时实现 C、A、P?
开发语言·后端·spring
疯狂成瘾者2 小时前
Java 常用工具包 java.util
java·开发语言·windows
ywl4708120872 小时前
springSecurity+jwt,简单版demo
java·前端·servlet