资讯动态

从0到1构建商用级无人健身房:设备接入、服务端设计与踩坑实录

发布时间:2026/9/10 19:37:11 来源:尧图企业网站定制
开篇先把话说透这个项目不是什么高深的理论研究就是一套能落地的商用级无人健身房解决方案。我前后帮朋友改造过三家共享健身房从第一版只会开关门禁到后来自动计费、能耗监控、设备故障预警全链路打通中间踩过的坑比写的代码多。如果你正准备做物联网方向的毕设、想入行 IoT 开发或者手头有个健身房/办公室/宿舍门禁想改造这篇文章应该能帮你少走两三个月的弯路。我会把整体架构、设备接入、Java 服务端实现、典型业务场景、故障排查全流程拆开讲代码只贴关键片段重点是讲清楚每个环节为什么这么设计以及哪些地方容易出事故。1. 需求拆解与整体架构无人健身房到底需要一套什么样的系统1.1 核心需求与业务边界无人共享健身房和普通健身房最大的区别在于没有前台没有巡场教练用户进门、使用设备、离开这一整套流程全靠系统自动完成。这就意味着系统必须覆盖三个核心环节身份鉴权与门禁联动、设备状态采集与控制、计费与订单闭环。第一个环节对应的是用户在小程序或 App 上下单后系统向门禁设备下发开锁指令同时记录入场时间这涉及设备联动和订单状态流转。第二个环节对应的是健身房里的跑步机、动感单车、力量器械需要上报运行状态包括开关状态、用电功率、累计使用时长这些数据一方面用于用户端显示设备占用情况另一方面用于异常预警和能耗统计。第三个环节是用户出场后自动扣费、生成账单如果超时未出场要有滞留提醒或延时段计费。很多人容易把这三个环节割裂来设计导致系统拆成三四个互不相通的子模块联调的时候到处踩坑。我的建议是在设计阶段就把设备接入、业务处理、数据流转三条链路统一起来设备侧只负责上报和接受指令业务侧统一通过消息队列或事件驱动的方式处理订单和设备状态这样后续扩展新设备、新业务时不会变成牵一发动全身。1.2 技术选型为什么是 Java Spring Boot Netty MQTT Redis这个项目的技术栈不是随便拍的我一开始也试过用 Python 快速搭一套原型但做到设备管理、并发计费、分布式部署这几个环节就明显吃力。最终定下来 Java 技术栈核心原因是物联网场景下的服务端需要长期稳定运行出问题要能快速定位而且用户端、管理端、设备端三端并发访问Java 生态对这些场景支撑成熟。Spring Boot 负责业务接口和整体应用骨架开发效率高生态丰富做订单、会员、设备管理等业务模块非常顺手。Netty 负责处理长连接和高并发 IO适合做设备接入网关的底层通信框架支撑大量设备同时在线时性能稳定。MQTT 协议负责设备端和服务端的消息通信轻量级、支持 QoS非常适合弱网和低功耗设备。Redis 负责缓存和分布式锁解决高峰期用户扫码、门禁控制、计费扣费等环节的并发问题。有人会问既然用了 MQTT为什么还要用 Netty我的理解是它们各管一段MQTT 解决的是设备与 broker 之间的协议通信Netty 解决的是业务服务端与消息系统之间的衔接以及服务端如何处理高并发连接。比如我们用 EMQX 作为 MQTT broker设备都连 EMQX而 Spring Boot 业务服务通过 Netty 做 TCP 长连接接入层统一接收设备上行的原始数据帧再经过协议解析转发给业务模块。1.3 整体系统架构分层设计整个系统我分成五层设备层、接入层、业务层、数据层、应用展示层。设备层就是健身房里的各种物理设备包括门禁控制器电插锁 门磁传感器、智能电表、跑步机控制器、空气质量传感器等核心主控选了 ESP32 系列原因是它自带 WiFi 和蓝牙还有多路 GPIO 可以同时接传感器和继电器价格不到二十块做健身房这种室内固定场景性价比极高。接入层负责设备接入和协议处理包含 MQTT broker 和 Netty 网关两个部分设备端通过 WiFi 连接路由器然后通过 MQTT 协议上报数据到 brokerNetty 网关作为服务端和 broker 之间的桥梁订阅设备主题并推送给业务层。业务层包含用户服务、订单服务、设备服务、计费服务等模块统一放在 Spring Boot 应用里。设备上报的数据到达业务层后会触发对应的业务流程比如门禁上报“已开门”订单模块就记录入场时间电表上报功率数据能耗模块就更新当前设备功率。数据层使用 MySQL 存储业务数据用户、订单、设备档案Redis 存储热点数据和设备实时状态时序数据如每分钟功率曲线可以先用 Redis 缓冲再异步批量写入 MySQL 或时序数据库。应用展示层包含用户小程序、管理后台、设备监控大屏三个终端。小程序面向用户代码里通过 WebSocket 或轮询接口获取设备状态管理后台面向健身房运营人员负责设置价格策略、查看营收、处理异常订单监控大屏面向运维人员展示门禁状态、设备在线率、告警信息。架构设计到这里最容易被忽略的是“设备唯一标识与业务绑定的映射关系”。每台设备要有唯一的 deviceId同时关联一个业务编号比如门禁设备编号、跑步机编号这些映射关系在设备出厂时初始化并在数据库中维护。否则设备上线后不知道是谁的上报数据也无法对应用户和订单。2. 设备端接入与通信ESP32 如何采集数据并与服务端稳定通信2.1 为什么统一走 MQTT 协议而不是 HTTP设备端和服务端通信很多新手第一反应是用 HTTP 接口让设备定时 POST 数据。听着简单实际操作一次就会发现一堆问题HTTP 是短连接每次请求都要建立连接设备多了服务端压力大弱网条件下请求容易超时数据丢了一旦没有重发机制后台看到的设备状态就是残缺的最要命的是服务端无法主动往设备下发指令比如用户扫码开门服务端要通知门禁设备开锁用 HTTP 就得让设备反复轮询体验极差。MQTT 协议恰恰解决了这几个痛点。它是基于发布/订阅模式的长连接协议设备连接 broker 后一直保持会话服务端可以随时向指定设备发布指令它支持 QoS 级别0 最多一次、1 至少一次、2 恰好一次可以根据数据重要程度选择不同的可靠性级别它还有遗嘱消息机制设备异常掉线时 broker 能主动通知服务端设备离线这让设备在线状态管理变得非常省心。我在项目中实际使用的是 EMQX 作为 MQTT broker部署在云服务器上开放 1883 端口给设备端连接。设备通过 WiFi 接入互联网然后使用 PubSubClient 库Arduino 环境下很常用连接 EMQX。设备上报数据的主题约定为gym/{deviceId}/upload服务端下发指令的主题为gym/{deviceId}/command设备上报在线状态的遗嘱主题为gym/{deviceId}/available。主题规划一定要提前定好不然后续每个设备一套命名维护成本翻倍。2.2 ESP32 端的数据采集逻辑与上报策略ESP32 在项目中扮演的角色是“采集 控制”一体。门禁控制器这边ESP32 通过 GPIO 控制继电器继电器再控制电插锁的通断电。门磁传感器一个简单的干簧管开关接在 GPIO 上用来检测门是打开还是关闭状态。当检测到门状态变化ESP32 会立即上报一条状态消息而不是等定时上报周期到了才发。这样做的好处是业务侧能实时感知用户进门和出门动作计费更准确。环境监测这一块我用了温湿度传感器DHT11 或 SHT30和空气质量传感器放在健身房的角落。ESP32 每隔 30 秒读取一次数据将温度、湿度、PM2.5 数值组装成 JSON 格式再上报。上报策略上我做了两类区分状态变化类事件门开关、设备启停立即上报周期采样类数据温湿度、空气质量固定周期批量上报。这种做法既保证了业务实时性又避免了大量无效上报阻塞 MQTT 通道。设备端代码的核心是建立 MQTT 连接并维护心跳。ESP32 上我真正常用的是 WiFi 的自动重连机制如果 WiFi 断开程序每 5 秒尝试重连一次WiFi 连上后检查 MQTT 连接是否还在如果断开了也自动 re-connect。因为健身房的路由器偶尔重启设备如果没有自动重连机制每次都要人工重启设备这个坑我在第一版踩过很多次。下面是 ESP32 端 MQTT 连接和数据处理的核心代码片段在 Arduino IDE 环境下开发#include WiFi.h #include PubSubClient.h #include ArduinoJson.h const char* ssid gym-wifi; const char* password 12345678; const char* mqttServer your-server-ip; const int mqttPort 1883; const char* deviceId GATE_001; WiFiClient espClient; PubSubClient client(espClient); void reconnectWiFi() { while (WiFi.status() ! WL_CONNECTED) { Serial.println(WiFi 断开重新连接...); WiFi.begin(ssid, password); delay(5000); } } void reconnectMQTT() { while (!client.connected()) { String clientId String(deviceId) - String(random(0xffff), HEX); if (client.connect(clientId.c_str(), gym_user, gym_pass, gym/ String(deviceId) /available, 1, false, offline)) { client.subscribe(gym/ String(deviceId) /command); client.publish(gym/ String(deviceId) /available, online, true); } else { delay(2000); } } } void callback(char* topic, byte* payload, unsigned int length) { String message ; for (int i 0; i length; i) message (char)payload[i]; if (strcmp(topic, (gym/ String(deviceId) /command).c_str()) 0) { if (message open_door) { digitalWrite(RELAY_PIN, HIGH); // 继电器吸合电插锁通电开锁 delay(3000); digitalWrite(RELAY_PIN, LOW); // 3 秒后断电自动上锁 } } }这里有个细节值得注意开锁指令不是直接保持继电器吸合而是触发后延时 3 秒断电。这是模拟“按一下开锁按钮”的效果——用户拉开门进去门磁检测到门打开后系统就知道用户已经入场不需要长时间保持门锁通电。如果一直给电不仅费电还容易烧继电器。2.3 设备在线状态管理的踩坑记录设备在线状态是无人健身房系统中很关键的基础数据。用户在小程序上看不到哪台跑步机在运转、哪台空闲体验会大打折扣运营后台也依赖在线状态判断设备是不是出了问题。第一版我把在线状态绑定在 MQTT 的心跳消息上设备每隔 10 秒发一次心跳服务端收到心跳就认为设备在线超过 30 秒没收到就判定离线。但实际运行中经常出现误判设备本身运行正常但因为 WiFi 信号波动偶尔延迟几秒后台就显示离线用户开始投诉“明明设备能用但小程序显示不可用”。后来我改成“MQTT 遗嘱消息 服务端超时检测”双重判定设备连接时设置遗嘱消息内容为 “offline”放在gym/{deviceId}/available主题上设备异常断线时 broker 会自动发布这条遗嘱设备正常运行期间每 15 秒在同一个主题上发布 “online” 消息设置了 retain 标志。服务端订阅这个主题一直收到联机消息则判断在线一旦 broker 发布了遗嘱消息就立刻判定离线。实际效果比纯心跳判断稳定得多因为 MQTT broker 的会话管理是协议层面保证的设备掉线触发遗嘱是即时行为不需要服务端逐步超时等待。这也说明了在物联网项目中合理利用协议特性往往比写一堆业务逻辑来兜底更可靠。3. Java 服务端核心实现从 Netty 网关到业务闭环3.1 Netty 网关层的作用与设计思路既然设备走 MQTT 协议业务服务用 Spring Boot那业务服务怎么接入 MQTT 消息最简单的方式是在 Spring Boot 里引入 MQTT 客户端库直接订阅主题。项目初期我就是这么做的但设备量上来后遇到几个问题Spring Boot 主业务线程被 MQTT 回调占用大量消息进来时业务接口响应变慢MQTT 客户端连接是单连接broker 消息量大时这个连接成为瓶颈不同模块订单、设备、告警都要监听消息代码耦合严重。于是我在 MQTT broker 和业务服务之间加了一层 Netty 网关。这层网关本身就是一个独立的 Java 服务负责两件事与 EMQX 建立 MQTT 连接订阅所有相关的gym/主题消息内部通过 Netty 维护一组与业务服务的长连接或者说通过共享的线程池处理消息把收到消息分发到对应的业务处理器。这样拆分后MQTT 连接管理和业务处理逻辑被完全隔离消息进入 Netty 网关后统一走解码、分发、异步处理三个步骤。网关层的解码器会把原始 byte[] 消息转成统一的 DeviceMessage 对象包含 deviceId、timestamp、type、payload 这几个字段后续业务模块只需要处理 DeviceMessage 即可不关心底层是 MQTT 还是 TCP。3.2 Spring Boot 业务服务的核心模块与流程设计服务端整体上是一个标准的多模块 Spring Boot 工程核心模块有 user-service用户、order-service订单、device-service设备、charging-service计费。实际代码中可能通过 Service 拆分成不同包但关键是要明确每个模块的职责边界。门禁开锁流程是整个系统最核心的链路拆解下来包含以下步骤用户在小程序点击“扫码开门”请求到达 Spring Boot 的/api/door/open接口。接口解析用户身份和当前订单状态判断用户是否有有效订单已下单且未结束。使用 Redis 分布式锁锁住 deviceIduserId防止同一扇门被同时请求多次。通过设备服务向 EMQX 发布开锁指令到gym/GATE_001/command主题消息体为{action:open_door}。门禁设备收到指令后执行开锁门磁检测到门打开设备上报门状态Spring Boot 收到消息后更新订单状态为“已入场”记录入场时间。用户出场时门磁检测到门打开系统记录出场时间触发计费模块生成账单扣除用户账户余额。计费模块设计上我比较推荐“按时计费 封顶价格”的模式。比如首小时 5 元之后每小时 3 元单日封顶 30 元。计算方式为public BigDecimal calculateFee(LocalDateTime startTime, LocalDateTime endTime) { long minutes Duration.between(startTime, endTime).toMinutes(); if (minutes 0) { minutes 1; } BigDecimal fee new BigDecimal(5.00); if (minutes 60) { long extraHours (minutes - 60 59) / 60; // 向上取整 fee fee.add(new BigDecimal(3.00).multiply(BigDecimal.valueOf(extraHours))); } BigDecimal cap new BigDecimal(30.00); if (fee.compareTo(cap) 0) { fee cap; } return fee; }这里有一个并发细节用户扫码开门时服务端需要判断用户账户余额是否足够如果余额不足直接拒绝开门。但用户可能同时开多个接口请求比如一个请求是检查余额另一个请求是创建订单中间没有事务保护就会超卖或重复扣费。我的处理方式是统一走 Redis 分布式锁 数据库唯一索引双重保障订单号使用雪花算法生成数据库对订单号加唯一索引这样重复请求会被数据库兜底拒绝。3.3 Redis 在系统中的应用场景不只是缓存Redis 在这个项目里不是锦上添花而是业务正常运转的底线设施。除了上节提到的分布式锁还有几个场景重度依赖 Redis。第一个是设备实时状态缓存。设备上报状态消息后服务端把最新状态写入 Rediskey 为device:status:{deviceId}value 为 JSON 字符串包含在线状态、功率、门状态等字段。前端展示设备列表时直接查询 RedisQPS 再高也能撑住不用每次请求都去查 MySQL。设备状态的更新频率高但容错性也高偶尔丢一帧问题不大所以这类数据完全不用落库只在状态变化时异步记录一份历史数据。第二个是用户入场状态缓存。用户进门后服务端把 userId、deviceId、入场时间写入 Rediskey 为user:checkin:{userId}同时设置过期时间为 10 小时防止用户忘记出场导致数据永远驻留。用户出场时直接删除该 key。这个缓存的作用是后续所有涉及用户状态的查询都只需一次 Redis 操作不用反复查数据库。第三个是防止设备消息重复处理。MQTT 的 QoS 1 保证消息至少到达一次也就是说可能重复。服务端处理消息时使用 Redis 的 setnx 机制对消息的唯一 ID由设备端生成比如消息时间戳设备ID随机数去重。重复消息来了直接丢弃避免重复更新订单状态或重复计费。这个机制我在实际运行中验证过非常有必要。3.4 服务端与设备之间的数据交互协议约定设备端和服务端之间的数据格式我觉得还是统一用 JSON 最简单虽然比二进制协议多了一些字节开销但对健身房的设备量级来说完全不是问题换来的是开发调试的巨大便利。设备上报消息统一格式{ msgId: a1b2c3, deviceId: GATE_001, timestamp: 1699999999, type: door_status, data: { doorState: open, trigger: user_enter } }服务端下发指令统一格式{ msgId: d4e5f6, deviceId: GATE_001, type: control, data: { action: open_door } }字段命名规范要从项目第一天就定死不然后面改起来全是泪。我踩过最深的坑是第一版设备端上报字段叫doorState到第二版业务侧重构统一改成了status结果忘了改设备端代码导致大量消息解析失败。后来我专门写了一个协议版本字段version服务端发现不支持的版本时记录日志而不是直接报错这类问题才变得可控。4. 典型业务场景实现从扫码进门到自动计费的完整串联4.1 扫码进门与门禁控制的场景实现扫码进门是用户使用无人健身房的第一触点体验如果做不好后面全是差评。这里说的扫码本质是小程序里根据健身房 ID 生成一个动态二维码用户到达门口用手机扫门上的二维码小程序自动跳转到当前健身房的入场确认页点击“确认入场”后请求后端接口后端校验用户身份和订单状态后下发开锁指令。这个流程里的一个关键点是“健身房 ID 如何绑定到门禁设备”。我的做法是在后台系统里做设备关联配置把健身房所在门店的 ID 与门禁设备的 deviceId 绑定。比如门店 A 的 deviceId 是 GATE_001那么用户扫的门店二维码对应的就是 GATE_001 的开锁指令。如果现场有多扇门入口和出口分离就配置多个设备同样关联到同一个门店 ID。开锁指令下发后服务端会开启一个 10 秒的超时等待窗口等待设备返回执行结果。如果设备在 10 秒内上报“door_opened”事件那就说明开锁成功否则服务端主动查询设备状态或者直接标记开锁超时前端展示“开门失败请稍后重试或联系管理员”。这里我做的优化是超时后不立即报错而是重试一次因为健身房门口的路由器偶尔延迟高指令到达设备有滞后重试一次往往就能解决。4.2 器械使用监控与能耗统计健身房里的跑步机、动感单车这类电动器械通常在插电口加装一个智能电表支持 WiFi 通信的就行电表实时采集电压、电流、功率数据通过 WiFi 上报到 MQTT。设备端把电表数据封装成统一格式上报服务端收到后做三件事更新设备实时状态、存储能耗历史数据、做异常用电判断。能耗统计这一块我一开始直接每次上报都写 MySQL结果高峰期电表每 10 秒上报一次单台设备一天就是 8000 多条记录几十台设备一天几十万行MySQL 的压力山大而且大多数数据根本没人看。后来我改成两个阶段原始数据先全部写入 Redis 的列表结构定时任务每 5 分钟批量将数据从 Redis 取出聚合后写入 MySQL 的能耗汇总表。汇总表的粒度是“每台设备每半小时的用电量”这样查询历史能耗时只涉及几千条记录报表随便出。异常用电判断的作用更大。比如某台跑步机理论上不使用时空载功率应接近 0但电表显示功率 200W说明设备可能没关或者某台设备功率一直波动剧烈说明内部有故障风险。服务端在收到功率数据后如果发现与设备的“标准运行状态”差异超过阈值会触发一条告警消息推送给运营后台和用户端。4.3 分布式场景下的并发控制与数据一致性无人健身房有多个门店、多台设备、大量用户并发操作数据一致性是绕不开的话题。最典型的场景就是“最后一台空闲跑步机被两个人同时预约”。用户端显示跑步机空闲两个用户同时刷新页面看到空闲状态然后同时点击预约。如果不做并发控制两台跑步机可能都会被预约出去甚至同一个人预约两台。我用 Redis 分布式锁解决这个问题预约请求到达服务端后先尝试获取 key 为lock:device:{deviceId}的锁获取失败就说明设备已被抢走返回“设备被占用”。获取到锁之后服务端再查询 Redis 中设备的当前状态确认状态为“空闲”然后执行预约逻辑更新设备状态为“已预约”最后释放锁。整个过程因为锁的存在不会出现两个请求同时读到“空闲”的情况。public boolean bookDevice(String userId, String deviceId) { String lockKey lock:device: deviceId; String requestId UUID.randomUUID().toString(); boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS); if (!locked) { return false; } try { String status redisTemplate.opsForValue().get(device:status: deviceId); if (in_use.equals(status) || booked.equals(status)) { return false; } redisTemplate.opsForValue().set(device:status: deviceId, booked); return true; } finally { if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }释放锁时判断是否为当前请求的锁是为了防止误删其他请求设置的锁这个细节我在生产环境里踩过坑当时的错误是直接用 delete 删除 key结果高并发下 A 请求的锁被 B 请求删除导致 B 请求还能再次获取锁并发控制完全失效。4.4 订单状态机设计避免订单流程失控无人健身房订单状态不是简单的“未支付/已支付”而是包含了入场、使用、出场、计费、异常等多个环节。为了不让订单流程绕成死结我设计了明确的订单状态机。订单状态包括created已创建未入场、entered已入场、exited已出场、charging计费中、completed已完成、abnormal异常单。状态之间的转换关系是created - entered - exited - charging - completed任意未完成状态都可能转入 abnormal。异常单的产生条件包括用户入场后超过 1 小时没有出场记录系统自动生成滞留提醒用户出场时门磁没有检测到门状态系统无法确定出场时间用户账户余额不足导致计费失败。异常单在后台专门有一个“异常处理”列表运营人员人工确认后可以手动修正订单状态修正操作记录日志方便追溯。状态机的实现我用了工厂模式定义一个 OrderState 接口每个状态对应一个实现类订单状态变更通过一个统一的 TransitionContext 上下文进行校验和转发。这样代码结构清晰后续加新状态比如“退款中”只需要新增一个实现类不会影响原有状态转换。5. 实战中的常见问题与排查实录5.1 MQTT 连接不稳定设备批量掉线的根因第一版上线后遇到一个诡异的问题每天早上九点到十点设备会集中掉线持续几分钟后自动恢复。最开始怀疑是路由器问题但检查了路由器日志发现没有异常重启。后来排查 MQTT broker 日志发现掉线时间点正好是健身房早高峰大量用户同时扫码进门服务端向设备发送了一批开锁指令这些指令通过 MQTT 下发导致 broker 的网络出方向流量暴增部分设备因为网络拥堵断线。这里的问题其实出在指令下发的频率和 QoS 选择上。我一开始把开锁指令的 QoS 设置为 2确保消息一定能到达设备但 QoS 2 会带来额外的确认流程高并发时 broker 压力增加。后来我把指令类消息统一改为 QoS 1对于开锁这种允许重试的场景足够可靠broker 的压力大幅下降批量掉线问题明显缓解。5.2 Redis 客户端报错increment 操作不是 integer 或 out of range这个错误出现得很突然线上日志突然大量出现ERR value is not an integer or out of range定位到代码是使用 RedisTemplate 的 increment 方法统计用户使用时长时触发的。原因是 Redis 中该 key 对应的 value 原本不是整数类型或者已经被其他业务逻辑写入了非数字字符串。排查过程是这样的我负责的计费模块里increment 操作是用于累加用户当日使用时长的key 设计为user:dailyMinutes:{userId}。按理说这个 key 只应该由 increment 方法操作但是另一段业务代码在更新用户信息时不小心对这个 key 执行了 append 操作导致 value 变成了字符串拼接结果再调用 increment 时自然就会报错。解决方法是调整 key 设计把不同业务的数据用不同的 key 前缀严格区分同时在代码里增加类型校验increment 前先检查 value 类型避免脏数据影响核心流程。5.3 设备上报消息重复导致重复扣费用户出场时门磁传感器因为信号抖动可能连续上报两三次“door_opened”事件。服务端收到第一遍就处理了出场逻辑第二遍再来时如果没有幂等处理计费模块就会发现同一订单被再次计费直接导致用户扣了双份费用。我解决这个问题用的是前面提到的消息唯一 ID 去重机制。设备端在生成消息时带上 msgId由设备 ID 时间戳 自增序号组成服务端处理消息前先用 Redis setnx 检查这个 msgId 是否处理过如果是重复消息直接丢弃。同时数据库层面订单表对“deviceId day orderId”加了唯一索引双保险兜底确保即便 Redis 偶尔异常数据库也不会写入重复数据。5.4 Netty 网关高水位导致的消息堆积Netty 网关运行一段时间后发现消息处理出现延迟部分设备上报的数据要过几十秒才进入业务服务。排查 Netty 日志时看到大量channelWritabilityChanged的告警这是 Netty 的写缓冲区水位过高导致的说明消息生产速度大于消费速度消息在 Netty 缓冲区里堆积。根因是 Netty 网关接收到 MQTT 消息后直接丢给了业务线程池处理但这个线程池的核心线程数配置得比较小默认是 CPU 核心数遇到高峰流量时线程池排队明显消息处理不过来。解决办法是把网关层线程池单独配置核心线程数根据设备量动态计算给了 16 个核心线程队列容量 10000同时在业务处理中引入异步消息队列这里实际用了内存队列把耗时的数据库操作和第三方调用都放到了单独的执行链路中网关层的处理速度大大提升。问题现象根因解决方案设备集中在某个时段掉线MQTT 指令 QoS 过高、broker 压力大指令改 QoS 1优化下发频率Redis increment 报错 not integer多个模块共用了同一个 key类型污染key 严格分区increment 前校验类型重复消息导致重复扣费门磁信号抖动重复上报msgId 去重 数据库唯一索引兜底Netty 消息堆积、处理延迟网关线程池配置不合理独立线程池异步化耗时操作5.5 无人健身房系统上线前的几个自检项根据几次上线经验我列一个简单的自检清单开发完系统照着过一遍能省下不少线上事故。第一设备断网自动恢复验证把 WiFi 断开观察设备是否能自动重连重连后是否会自动重新发送遗嘱消息和上线消息服务端是否能正确恢复设备在线状态。这个测试不做线上路由器重启一次就可能出现一批设备“僵尸化”状态与实际不符。第二重复消息压力测试用脚本模拟同一个 msgId 的重复消息确认服务端不会重复更新订单状态或重复计费。硬编码造 200 条相同 msgId 的消息打过去看数据库有没有产生重复数据。第三门禁指令超时重试验证模拟开锁指令下发后设备不响应确认服务端超时逻辑正常第二次重试能成功不影响用户扫码体验。第四高并发场景压测用 Jmeter 模拟 100 个用户同时扫码进门观察接口响应时间、数据库连接池用量、Redis 连接数是否在合理范围。提前 5 分钟启动定时任务清点设备状态避免压测数据污染正式数据。结尾这套系统从需求梳理到落地商用前后迭代了三版才算稳定期间踩过的坑两只手数不过来。我个人最大的体会是物联网项目比纯软件项目复杂的地方在于你不仅要写逻辑还要跟硬件、网络、物理环境打交道一个继电器质量差可能就能让你的门禁系统隔三差五抽风。如果你从零开始做建议先跑通一条最小闭环哪怕只控制一盏灯从设备上报到服务端处理到页面展示全流程跑通后再扩展其他功能。这条链路的每一步都有讲不完的细节但也正是这些细节才把一个“能演示的项目”变成真正“能商用的产品”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价