1. 项目概述与整体设计思路
1.1 这个项目到底解决什么问题
这两年做物联网平台的后端研发,最典型的场景就是:设备接入、指令下发、状态上报、数据可视化。如果你用Spring Boot自己从零写一套,设备管理、用户权限、菜单配置、操作日志、代码生成这些“管人”的东西全都得自己写一遍,至少要多花两到三周时间。而市面上现成的前后端分离框架里,RuoYi(若依)是Java技术栈里被验证过很多次的那一个:权限体系完整、代码生成器顺手、定时任务/日志/字典管理都齐了。把MQTT协议接入层叠上去,就构成了一个能落地的物联网管理平台。
项目标题里的“V2”,意味着这套源码不是第一次跑通就停了。V1阶段往往存在几个问题:设备上行数据直接落库、没有做指令缓冲、设备掉线状态靠轮询数据库判断。V2在这一基础上的核心变化是:用MQTT Broker做消息中枢,设备在线状态改由遗嘱消息维护,下行指令通过异步任务持久化后再推送,配合若依自带的自动化代码生成器把重复性业务接口批量产出。整个平台的方向是让Java开发者在拿到源码之后,不必纠结“权限模块怎么办”,而是把精力放在设备接入和业务处理上。
1.2 为什么是Spring Boot + MQTT + 若依这套组合
先拆开说技术选型。
Spring Boot是骨架层。物联网平台的业务面很大:设备管理、设备分组、告警记录、工单派发、用户管理。Spring Boot生态里的spring-boot-starter-data-redis、mybatis-plus、spring-boot-starter-validation这些组件组成了平台的业务底座。你不必自己造轮子去管理Bean生命周期和数据库连接池,框架已经替你解决了。
MQTT是设备通信层。选MQTT不选HTTP,主要是因为物联网设备的网络环境不可控:弱网、掉线、高延迟。MQTT基于TCP长连接,配合QoS机制能保证消息不完全丢失;遗嘱消息(Will Message)能在设备异常断开时立刻感知;发布/订阅的Topic模型天然支持设备和平台之间的解耦。实际项目里,哪怕是某厂房里有上百个温湿度传感器和几十个阀控设备,单台硬件的负载也是完全扛得住的。
若依是业务加速层。RuoYi开箱即用带了部门管理、角色权限、菜单管理、操作日志、代码生成。尤其那个代码生成器,连表结构之后能自动生成Java后端代码和Vue前端页面。对物联网这种实体多(设备、传感器、分组、告警规则、联动策略)的项目来说,减少CRUD手写量是实实在在的提效。
1.3 整体架构的数据流向
用最直白的方式描述这套平台的数据链路:
传感器设备--MQTT协议-->Broker(MQTT消息服务器)-->MQTT订阅监听器-->消息解析与规则判断-->业务落库/Redis缓存-->WebSocket推送到前端大屏 前端页面--HTTP请求-->Spring Boot业务接口-->Redis/DB--指令下发表-->MQTT发布-->设备执行器后面所有章节的内容,本质上都在围绕这条数据链路展开。设备上行之后如何保证数据不丢且不乱,下行指令如何确保设备必然收到、如何防止重复下发,这些才是平台真正的设计难点。
2. 环境准备与基础工程改造
2.1 基础服务部署清单
先把这套平台依赖的外部中间件列出来,按优先级排序:
| 组件 | 版本建议 | 用途说明 |
|---|---|---|
| JDK | 1.8+ | 若依原生支持,V2我在项目里也跑过Java 11,没问题 |
| MySQL | 5.7或8.0 | 设备元数据、用户权限、告警记录、指令日志的持久化存储 |
| Redis | 5.0+ | 设备影子缓存、验证码、在线状态临时存储、消息幂等去重 |
| MQTT Broker | 建议EMQX 4.4+ | 消息接入、遗嘱处理、主题管理,选它是因为开源、文档全、插件丰富 |
| Nginx | 1.18+ | 前端静态资源与接口反向代理,WebSocket走ws://路径需要额外配置 |
这些服务部署齐了之后,先把RuoYi原生系统拉起来验证环境通不通,再着手改造。我见过不止一次,一上来就改MQTT代码,改到一半发现连不上MySQL,排查半天居然是密码文件里的空格问题——基础没打好,后面全是坑。
2.2 引入MQTT依赖与核心代码骨架
在pom.xml中新增的依赖主要是Eclipse Paho Java客户端。这是Java生态里最普及的MQTT客户端库,跟Broker配合稳定、坑少。
<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency>很多人不明白为什么还要加spring-boot-starter-integration。这里其实是个经验之谈:RuoYi的项目结构里已经存在ruoyi-common、ruoyi-framework等模块,而Spring Integration对MQTT提供了MqttPahoMessageDrivenChannelAdapter这种现成的消息驱动适配器,能把“订阅-监听-分发”整个流水线托管。你如果只想用原生Paho,当然也能写,但消息到达之后的线程管理、异常重连、通道转发这些都要自己实现。在V1版本里我用的是原生Paho,后来后续架构重构时换成了Spring Integration的抽象层,省了不少事。
随后建立一个MQTT配置类,核心是把连接参数整理成MqttConnectOptions:
@Configuration @ConfigurationProperties(prefix = "mqtt") public class MqttConfig { private String host = "tcp://127.0.0.1:1883"; private String clientId = "iot-platform-server"; private String username; private String password; private Integer timeout = 30; private Integer keepalive = 60; @Bean public MqttConnectOptions mqttConnectOptions() { MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(true); options.setConnectionTimeout(timeout); options.setKeepAliveInterval(keepalive); if (StringUtils.hasText(username)) { options.setUserName(username); } if (StringUtils.hasText(password)) { options.setPassword(password.toCharArray()); } options.setAutomaticReconnect(true); return options; } }setCleanSession(true)要特别说到。对于平台服务端,我们的诉求是每次重启之后重新订阅所有Topic,而不是接着上次的会话缓存继续收。这样设计的好处是平台侧逻辑简单、状态不残留。代价是离线期间的消息会丢,但对于管理平台而言,设备上报数据本身会再通过业务表持久化兜底,所以可接受。设备侧则通常设成false,保证离线消息在重连后有补偿机会。
2.3 如何用若依的自动化代码生成快速搭建第一个业务模块
若依的代码生成器是整个V2平台里高效的开发工具。具体操作流程是这样的:
- 在MySQL里创建业务表,比如
iot_device,字段尽量规范:主键device_id、设备编号device_code、设备名称device_name、型号device_model、状态status、创建人与创建时间。 - 在若依后台菜单中找到“系统工具-代码生成”,点“导入”,选择刚才的
iot_device表。 - 系统会自动读取表注释和字段注释,生成Java实体类、Mapper、Service、Controller以及Vue页面。
- 编辑配置时需要注意:主键策略选“自定义”,因为设备编号往往是业务决定而不是自增ID;列表显示勾选设备编号、名称、状态;查询方式把设备编号设为“EQ精确匹配”,名称设为“LIKE模糊匹配”。
- 生成代码后,将文件分别放到
ruoyi-system模块和前端src/views目录下,重启前端后菜单里自动出现设备管理页面。 - 最后在角色管理中赋权,开发机和测试账号都有了完整的“设备管理”入口。
这一步操作完成后,你已经有了一套带权限控制、支持增删改查、能关联部门数据的设备管理CRUD。注意这里只是元数据管理,真正的设备接入通信其实还没开始——这个差异必须拎清楚,很多新手把RuoYi生成的“假设备表”当成了物联网设备接入能力。
2.4 基础工程改造的三点实操心得
第一,关于数据源路由。设备运行数据和平台配置数据建议从一开始就分库。不要犹豫,我在V1后期为这个改动付出了几周的代价。RuoYi本身加多数据源不难,在application.yml里配置第二个数据源,然后通过自定义@DataSource注解切换。建议把iot_device_status、iot_device_data这些高频读写表放在独立库,避免后台菜单操作锁影响到设备数据写入。
第二,关于MyBatis-Plus。如果你坚持用原生MyBatis写每一条SQL,开发效率会明显偏低。在RuoYi的基础上集成MyBatis-Plus是常规操作,在pom.xml加依赖后,把Mapper接口继承BaseMapper<T>即可获得现成的selectById、selectList、insert方法。只有复杂统计或联表场景手写SQL,其余全交给Plus。此时代码生成器的产出质量也会有提升。
第三,关于统一响应结构。RuoYi的AjaxResult已经很完善,但物联网平台会有很多“下发指令”“查询设备状态”这类接口,建议再建一层MqttResult,内部包含success、code、data、message,且data中强制包含traceId。有了traceId,在线排查问题才能把“HTTP请求-数据库记录-MQTT消息”链起来。
3. MQTT接入层核心设计
3.1 Topic主题规划
设备消息通信的Topic规划会直接影响平台的扩展性和可排查性。在V2版本中,我设计了一套统一主题规则,以斜杠分层:
iot/{productCode}/{deviceCode}/up -- 设备上行数据上报 iot/{productCode}/{deviceCode}/down -- 平台下行指令下发 iot/{productCode}/{deviceCode}/status -- 设备状态变更通知 iot/{productCode}/{deviceCode}/event -- 设备事件(告警、异常)边的+号适配这一层所有设备,#号匹配所有后续层级,例如:
iot/+/+/up这样一条订阅就能监听所有产品的上行数据。在业务开发中,产品品类多的时候还可以用通配符快速做数据接入。
Topic规范化设计的核心收益是排查问题方便。比如某型号设备上报异常,直接去Broker后台查看iot/{该型号}/+/up这个主题的消息流量,能快速定位是这个型号整体上报频率过高,还是单台设备行为异常。
3.2 消息监听、解析与协议适配
订阅端的实现方式取决于你选的是自然Paho还是Spring Integration。用Spring Integration的方式更简洁,核心代码如下:
@Bean public IntegrationFlow mqttInboundFlow() { return IntegrationFlows .from(new MqttPahoMessageDrivenChannelAdapter( mqttClientId(), mqttBrokerUrl(), "iot/+/+/up", "iot/+/+/status", "iot/+/+/event")) .handle(message -> { String topic = message.getHeaders().get("mqtt_receivedTopic").toString(); String payload = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8); mqttMessageDispatcher.dispatch(topic, payload); }) .get(); }这个监听器里不写任何业务逻辑,只做两件事:从消息头里取真实主题,将字节数组转字符串,然后统统交给dispatcher去分发。职责分离的含义就是:无论以后接入什么类型的设备,这个入口保持不变,业务分支在dispatcher里体现。
协议适配是消息解析里最需要重视的部分。真实物联网项目里协议千奇百怪,两种最典型的情况,一套业务平台的代码要同时兼容:
- JSON明文格式:比如
{"temp":25.6,"humidity":60.2},解析简单,适合开发期调试。 - 二进制透传格式:设备厂商自定义,比如前两个字节是帧头
0xAA 0x55,第三个字节是功能码,后面是数据位和CRC校验位,解析时必须按字节序切割。
我在实际项目中处理过一种情况:某型号湿度传感器因为填充规则不统一,相同的一个字段在不同固件版本里用整数和小数两种方式传输,导致数据入库后漂移无法解释。后来在解析层做了协议版本识别,根据消息里携带的fwVersion字段决定解析规则。所以消息解析层一定要设计成策略模式,每种协议对应一个MessageParser实现类,平台才能应对后续新增设备的兼容。
3.3 下线状态判断与遗嘱消息
在线状态是物联网平台最基础的能力之一。V1里用数据库记录心跳时间,定时任务扫描,效果差到极点:延迟高、数据库压力大、状态不准。V2里完全推翻重来,改用MQTT的遗嘱机制 + Redis过期时间做双保险。
设备连接时携带遗嘱消息:
MqttConnectOptions options = new MqttConnectOptions(); String deviceClientId = "device_" + deviceCode; options.setCleanSession(true); options.setWill("iot/" + productCode + "/" + deviceCode + "/status", "{\"status\":\"offline\"}".getBytes(StandardCharsets.UTF_8), 1, false);平台端订阅这个/status主题,收到内容后更新Redis中的设备状态,并标记为离线。设备正常在线时,还会按照保持周期定时上发心跳包(或着说平台根据MQTT底层的心跳包来判断),平台收到任何上行消息都会刷新Redis里这张设备在线状态的过期时间。
双重机制的设计理由很明确:MQTT遗嘱虽好,但只能在连接非正常断开时触发,如果设备进程被强杀或网络闪断,遗嘱消息确实会发出;但有些场景Broker没有及时感知到连接丢失,遗嘱也不会立刻发布。此时Redis里的过期时间兜底,把超过N秒没有数据上报的设备在查询时自动标记为离线。V2实测下来,状态准确率能到99.5%以上,延迟控制在秒级。
3.4 数据上行后的处理线程池
MQTT监听器收到消息之后,如果直接在Paho回调线程里做业务处理,会导致回调线程阻塞,进而影响Broker和客户端之间的心跳应答。正确姿势是线程池异步处理:
@Bean("mqttBizExecutor") public ThreadPoolTaskExecutor mqttBizExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(2000); executor.setThreadNamePrefix("mqtt-biz-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; }CallerRunsPolicy是这里的关键,当消息量突增、任务队列满时,它会把任务回退给Paho线程执行,而不是直接丢弃消息。在设备大规模上线或网络恢复瞬间,会有大量积压消息瞬时到达,这种策略能有效防止消息丢失。
此外,对于上行数据的解析与落库,还存在幂等性问题。因为MQTT QoS=1本身是“至少一次”,重复投递不可避免,所以平台在Redis里以msg:{topic}:{payloadHash}为key做判重,落库之前先检查这个key是否已存在,设置短过期时间比如2分钟。实测这样处理之后,重复数据占比大幅下降。
4. 业务功能:设备管理、设备影子与指令下发
4.1 设备元数据与业务建模
RuoYi代码生成器产出的第一版设备表只有最基础的字段,但实际项目里还应该补充以下维度:
- 产品维度:
product_code、product_name,一张产品表,对应同一批同型号设备 - 地理维度:
longitude、latitude、address,地图展示时要用 - 归属维度:
dept_id,若依原本就有部门关联,物联网设备也要归属到部门,比如某个车间或某个站点 - 生命周期字段:
activation_time、last_online_time、firmware_version,用于设备生命周期管理与远程升级 - 密钥字段:
secret_key,设备认证使用
表格设计可以用一张设备表和一张产品表,加上若依原本的sys_dept部门表,形成完整的三层归属关系。我在项目中验证过,这套模型处理上千台设备、几十个部门完全没有问题。
4.2 设备影子与状态缓存
设备影子(Device Shadow)这个概念的来源是:设备可能处于网络波动状态,平台下发的配置或状态数据需要有一个“暂存区”,等设备上线之后取走。实现有很多种,Redis是既简单又高效的一个:
public void updateDeviceShadow(String deviceCode, Map<String, Object> props) { String key = "iot:shadow:" + deviceCode; redisTemplate.opsForHash().putAll(key, props); redisTemplate.expire(key, 7, TimeUnit.DAYS); }把iot:shadow:{deviceCode}设计为一个Hash结构。设备最新上报的温湿度、开关状态、运行模式都放在这里;前端展示设备详情时,不用每次都查设备上报历史表,直接从这个Hash里取,速度极快。同时也为指令下发提供了状态依据:比如控制设备前先检查影子里的开关状态,如果已经是目标状态就不用重复下发。
这个设计在指令风暴场景下价值很大。例如某批空调设备,操控页面上一个“批量设置26度”动作如果直接落到设备,可能一次推送几百条消息。配合设备影子可以做“设置前比较”:先比较目标值与影子值,相同则忽略。实测这个策略能把无效指令量降低40%左右。
4.3 下行指令的实现细节
在V2平台里,下行指令整体链路是这样的:前端请求后端的指令下发接口,后端先写一条iot_command记录,状态为“待发送”,然后异步通过MQTT发布指令,Broker确认投递后,再回写状态为“已发送”;若设备有应答指令,则进一步更新为“已完成”。
public void sendCommand(String deviceCode, String commandTopic, String payload) { MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); message.setQos(1); message.setRetained(false); mqttPublisher.publish(commandTopic, message); }QoS的选择是这里最重要的决策点。V2统一采用QoS=1,理由很直接:QoS=0会丢消息,不适合控制类指令;QoS=2虽然保证不重复,但会增加握手延迟且部分Broker对QoS=2的支持不理想,不适合高频控制。至于“重复”问题,交给业务层的幂等校验解决。具体做法是在指令的payload里带上commandId,设备端在执行前检查最近执行过的commandId集合,重复的指令直接忽略应答。
还有一点容易被忽略:指令下发必须做事务处理。数据库指令表的状态写入和MQTT发布不能天然一致。如果MQTT发布先成功、数据库记录写入失败,那么重启后就会出现指令记录缺失但设备端已经执行的情况。所以代码里写了事务步骤,先落库再发布,发布失败时自动重试,重试超过三次再标记“下发失败”,保证对账有记录可查。
4.4 多租户与权限控制
若依框架的权限基于RBAC模型,部门间数据隔离靠Dept。在物联网场景里,这套逻辑可以无缝扩展。“某公司A的车间管理者登录后,只能看到本公司部门下的设备和数据”,这正是若依原有权限体系带来的红利。
在Device表的查询Mapper中,加入部门过滤逻辑:
<select id="selectDeviceListByDept" parameterType="Device"> select d.* from iot_device d left join sys_dept dept on d.dept_id = dept.dept_id where 1=1 <if test="params.deptId != null"> and (d.dept_id = #{params.deptId} or d.dept_id in ( select dept_id from sys_dept where ancestors like concat('%', #{params.deptId}, ',%') )) </if> </select>子部门看到子部门的设备,父部门默认也能看到所有子部门的设备。这个数据权限设计的边际成本几乎为零,却让平台具备了面向多客户、多组织独立运营的基础。
5. 自动化代码生成的深度实践
5.1 若依代码生成器的重新认识
很多人把若依代码生成器简单理解为“生成CRUD”,但V2项目里应该把它当成一种“半成品模具”。直接用生成的代码交付只能应付最简单的内部系统,真正的价值在于改造。前面说的iot_device表生成之后,对于一张标准的“设备信息表”,需要改造的地方一般有:
- Service层:在插入和更新之前校验设备编号唯一性
- Controller层:增加
@PreAuthorize("@ss.hasPermi('iot:device:control')")这样的细粒度权限注解 - Mapper层:补充关联设备影子表和指令表的逻辑
如果未来要大批量生成新模块,比如“告警记录表”,那么精力只需放在“告警规则怎么从Device表读取”这一个点上,其他的表单、分页、导出通通不必关心。批量生成多个模块的节奏感会让你觉得开发速度至少提升两倍。
5.2 模板改造:让生成器理解物联网实体
若依的代码生成器自带模板文件,存放位置在resources/vm/java/下的domain.java.vm、serviceImpl.java.vm等。对这些模板做定制化修改,能让你后续每次生成物联网相关表时自动附带该有的逻辑。
以domain模板为例,我在模板中添加了公共字段自动填充逻辑:
@TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime;同时,在Service实现模板里预留了createBy等审计字段的赋值代码。这样每次生成新表时,自动就有了审计能力。项目里本身有十几类小表,如果每张表手写这些字段,仅仅是无意义的重复劳动。
5.3 前后端同时生成,减少接口联调成本
RuoYi代码生成器不仅是后端生成器,前端Vue页面也一并生成,包括列表页、表单页、对话框。对比从零手写Vue页面,代码生成器生成的是可运行的基础版。改造时需要关注的重点样式如下:
- 列表筛选区:设备编号用输入框,状态字段用下拉选择框,时间范围用日期范围组件
- 表格列:设备状态列用
tag标签显示,颜色随状态变化——绿色在线,灰色离线,橙色告警 - 弹窗表单:设备编号默认禁用编辑,创建时自动生成UUID,不允许手填
这些前端模板的默认生成质量在最初使用时略感平淡,但配合若依的字典标签就能迅速精美起来。把“设备状态”做成字典,代码生成器会自动把状态列渲染成dict-tag标签组件,代码和交互都很干净。
5.4 自定义业务模板的落地实例
以平台中最常用的“设备指令表”为例。这个表的职责是记录每一条下行指令的下发历史。若依自带的模板对这种“操作日志型”表支持不够,需要自定义模板。
我的做法是:在resources/vm/java/目录下新建一个commandService.java.vm模板,关键逻辑是重写insert方法,保证指令状态初始为“待发送”,同时生成一个command_id(UUID字符串)。这样,凡是与指令相关的日志表,都通过这个模板生成Service,天然具有统一的发指令能力。这比每次手写Service再抽公共方法更高效——因为表结构都是固定的:command_id, device_code, command_type, payload, status, create_time, finish_time, remark。
自动化代码生成在V2项目中真正发挥威力后,一周内我就完成了设备分组、告警记录、维护工单、固件版本四张表的全部后端接口和前端页面,且风格统一、可维护性高。若依官方文档其实只写了“如何用”,这一节重点分享“如何改”,价值就在这里。
6. 常见问题、性能优化与安全加固
6.1 高频踩坑问题速查表
V2平台从开发到部署生产,遇到过的典型问题远不止十个。这里选最有代表性的几个,整理成表供大家直接参考:
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
| 设备长时间不重连,重启服务后收不到数据 | cleanSession为true且服务端离线期间消息被清除 | 确认是否需要持久会话,或者让设备重新上报一次影子数据 |
| QoS=1消息重复导致设备重复执行 | Broker重发机制 | payload增加commandId,设备端做幂等判断 |
| 上行消息持续积压在队列,任务堆积 | 线程池排队策略设置不当 | 设置合适的队列容量,采用CallerRunsPolicy兜底 |
| 设备频繁断开重连 | keepalive时间过短,弱网环境心跳超时 | 根据网络情况增大keepAlive间隔(如60秒或120秒) |
| 设备影子数据更新异常,显示旧状态 | Redis中Hash过期时间设置过长/过短 | 交易一致性:上行数据更新后同步覆盖Hash,过期时间设置为从最后活跃时间起算 |
| MQTT发布者在并发下偶发连接占用 | Paho客户端非线程安全 | 统一使用MqttPublisher单例,内部加锁或使用MqttAsyncClient |
上表里的第五个问题“设备影子更新异常”最适合多解释一句。当时某厂房的网关设备更新配置后,前端页面上显示的仍是旧版本配置,排查后发现是因为影子表的过期时间设置成了7天,配置更新后没有主动清掉旧影子键。后来改成每次更新配置时同步调用updateDeviceShadow,把最新值写入Hash并刷新过期时间,问题立刻消失。
6.2 性能优化:从单机到集群
V2版本在单台Broker上的表现能满足中小规模场景。粗略的实测数据是:一台常规配置的Broker(4C8G)在测试环境稳定处理每秒5000条左右的上行消息,CPU占用率在中低水位,内存稳定。如果你更复杂,这里有几个可落地的优化方向:
- 消息分流:将上行数据的“解析”与“业务处理”彻底拆成两条线程池。解析线程只做格式转换,然后把标准化的数据对象放进内存队列(如
Disruptor或LinkedBlockingQueue),业务线程池再落库。这规避免了解析慢影响吞吐的问题。 - 批量落库:设备上行的高频数据不要每一条都写一次数据库。用
List积攒100条或1秒内的数据,再做批量插入,实测写入性能提升3~5倍。 - Redis做热点数据缓存:读操作尽量走Redis,每次查询设备状态都打数据库的写法,在设备规模增大后会直接压垮数据库。
如果单台Broker不够,可以使用EMQX的集群能力,通过共享订阅把消息负载分摊到多个节点。而平台侧需要做的只是把mqtt.host配置成一个负载均衡地址就行,应用本身无状态,可以随意水平扩展。
6.3 安全加固:设备认证与TLS加密
物联网平台如果没有设备认证,任何人都可以发布消息到你的Topic,进而伪造设备上报数据或控制设备。V2的安全体系做了三层:
- 第一层,设备级认证。在设备启动连接时,Broker开启密码认证,每个设备发放独立的
username(设备编号)和password(设备密钥)。平台启动时从数据库读取设备密钥,调用Broker管理接口动态添加认证信息。实测EMQX可以通过HTTP API创建用户和ACL规则,整个过程自动化无需人工干预。 - 第二层,ACL访问控制。设备只被允许发布和订阅属于自己的Topic。例如设备A不能订阅
iot/{productB}/{deviceB}/down,否则就可能窃听到其他设备的控制指令。在Broker配置ACL规则,按主题前缀拦掉无效访问。 - 第三层,TLS加密传输。设备在网络链路传输中避免明文可读。证书配置的方式几句话说不完整,但核心思路是:在Broker侧配置SSL证书,在设备侧注入根证书,客户端连接地址改为
ssl://或tls://协议前缀。如果设备端资源极受限无法跑TLS,至少要启用用户名密码认证,并内网专线接入。
6.4 生产环境的监控与告警
平台上线后,监控是运维的重要指标。V2里我需要盯的几个核心指标包括:
- Broker连接数:设备连接规模超阈值时预警。
- 消息流入速率:每秒钟进入Broker的消息数量,如果突然暴增,可能就是某批设备逻辑异常。
- 消息积压量:下游消费线程池积压的任务数,任务数超过队列容量就需要告警。
- 指令下发成功率:通过数据库统计
iot_command表中“已发送/已下发/已完成”的比例,低于某个阈值就提醒。
这些指标可以通过若依的定时任务模块定时采集,用已有的Job基础设施,每个定时任务对应一个监控指标。写完后记得配置告警订阅,比如推送到企业微信或钉钉群,运维人员能及时发现问题。这部分能力不需要额外引入重量级的监控系统,在中小项目里Jet直接有效。
7. 扩展玩法与最终个人体会
到这里,整个平台已经跑通了从设备接入、数据上收、指令下发到业务管理的完整闭环。基于这套代码继续往下延展,有几个方向很值得尝试:
规则引擎。在设备上行消息的解析环节,提供引擎让维护人员用可视化配置方式编写“当温度大于X且持续N分钟,就触发报警”,不必修改Java代码。这会直接提升业务的自主配置能力。
大屏可视化。设备数据进Redis之后,后端通过WebSocket推送数据,前端只需订阅对应通道。RuoYi网关层默认支持WebSocket,不过需要做鉴权改造,防止未登录用户直接拉取大屏数据。
设备OTA升级。在指令下发的基础上增加固件包管理和分片下发,让设备在不停止运行的情况下完成固件升级,是商业项目中很值钱的功能。
最后说一下个人在实际操作中的体会。V1到V2最大的转变,不是换了一个消息库还是增加了一些缓存策略,而是把整个工程的视角换了。V1里我在思考“这个设备上报的数据怎么存进数据库”,V2里我在思考“整个系统的数据怎么流转、怎么不丢、怎么可控”。写这套平台最关键的节点不是深夜把最后一块代码写完时的那种兴奋,而是上线第一个月里看着它稳定运行、数据始终准确时那种平实的踏实。如果你正在做物联网平台相关的项目,我希望这篇文章里关于Topic规划、遗嘱消息、设备影子、幂等设计的这些经验能帮你少走一段我之前走过的弯路。若依这样的框架确实让开发速度快了很多,但真正让平台像平台的,是你对通信协议和业务逻辑的掌握程度。