资讯动态

基于Java与MQTT的物联网无人售货系统架构设计与核心实践

发布时间:2026/9/10 7:30:31 来源:尧图企业网站定制
说实话我第一次完整跑通“物联网结合 Java 无人售货系统”的时候最大的感受不是技术有多难而是这个题目比想象中要重得多。它既不是简单的“Java 电商后台”也不是纯硬件的“单片机点灯”而是要把设备端、网络通信、服务端业务、支付对账全部串成一条闭环。尤其当你真正把设备铺到线下遇到“订单显示成功、货没掉下来”“设备离线但用户还在扫码付款”这类问题才会意识到这套系统真正的核心是设备与业务之间的可靠协同。这篇内容适合正在做物联网相关毕业设计、想入行 IoT 后台开发、或者公司准备自研无人货柜/售货机的朋友。我会把整套系统的设计思路、通信协议、核心链路、常见坑位都讲透尽量还原一个真实项目从 0 到 1 的完整过程。1. 整体设计思路与架构选型1.1 无人售货不是“电商页面 扫码支付”很多人第一次接到这个需求第一反应是做一个商品列表页面用户扫码进去选商品、支付然后让售货机出货不就完事了吗真这么干项目上线第一天就会被现场运营骂死。无人售货系统本质上是一个物联网应用它的核心矛盾在于“物理世界的不确定性”。你在网上买一个商品订单状态是纯数字世界的状态但无人售货机卖一瓶可乐牵扯的是实打实的机械动作货道电机有没有转、商品有没有掉下来、掉下来的是不是用户选的那个、掉下来之后会不会卡住。这些问题任何一个环节和数字世界的订单状态对不上都会产生客诉。所以我在设计系统时第一原则就是把设备当成一个不稳定的远程对象来管理而不是一个可以同步调用的内部服务。围绕这个原则整个系统分成了四层设备层主控板货道驱动板支付模块网络模块负责实际出货和状态采集。接入层负责设备连接、鉴权、心跳、消息上下行是物联网系统的心脏。业务层订单、库存、商品、支付、退款、会员、补货管理跟普通电商系统类似。运营层设备监控、远程运维、数据分析、告警中心。这种分层看起来平平无奇但它决定了后面所有的技术选型。设备层和接入层之间的通信不能随便用 HTTP因为售货机大量部署在商场、园区、学校网络环境不稳定设备 IP 是动态的服务端根本没法主动直连设备。必须采用设备主动连上来然后靠长连接维持双向通信的模式。这就是物联网里最常见的 MQTT 协议要解决的问题。1.2 为什么技术栈选了 Java MQTT Netty先说结论这组组合在无人售货这个场景里非常稳不是因为它最潮而是因为它把“设备通信”和“业务开发”这两件事彻底分开了各干各的活。Java 生态扛业务层是轻车熟路。订单、支付、库存、后台管理Spring Boot 全家桶能解决 90% 的问题招人也容易后面接手的同事不会看不懂。真正的关键是接入层怎么选。MQTT 协议是目前设备接入的主流标准它专为低带宽、不稳定网络设计支持 QoS 消息等级、遗嘱消息、主题订阅。无人售货机这种嵌入式设备跑 MQTT 非常合适连接轻量心跳保活断线能感知。但 MQTT 只是协议标准落地还要有 Broker消息代理服务器。在小规模阶段可以直接部署一个开源的 EMQX等设备量大了或者你希望把消息处理逻辑牢牢握在自己手里可以用 Netty 自己写一套 MQTT 接入网关。我自己做过的方案是初期直接用 EMQX同时用 Netty 做设备指令下发和状态同步的网关——这里解释一下为什么还要 Netty。设备端上报数据是异步的服务端要控制设备出货也是异步的。MQTT Broker 只解决消息转发但业务系统需要一种“把指令发给指定设备并且等它回执”的机制。Netty 在这里可以拿来处理高并发的长连接也可以做自定义的指令网关。实际项目中更常见的做法是设备统一走 MQTT 接入 EMQX业务服务通过 EMQX 的 HTTP API 或直接订阅主题来收发消息Netty 则用于自研接入服务或者对接非 MQTT 协议的设备比如老款 2G 模组走 TCP 私有协议的。我个人的建议是如果你的项目是毕业设计或者中小规模商用不要一上来就自研 MQTT Broker那是浪费时间。用 EMQX Spring Boot Redis MySQL 这套组合快速把业务闭环跑通才是关键。1.3 整体架构和核心模块边界我在项目里画的架构图文字版给你们描述一下大概是这样的设备端ESP32/STM32 4G模组/以太网 | | MQTT(TLS) v EMQX Broker设备接入主题管理 | | MQTT/HTTP API v IoT 接入服务Spring Boot - 设备鉴权、状态管理 - 消息解析、指令下发 | | 内部接口/消息 v 业务服务Spring Boot - 用户、商品、订单、支付 - 库存、补货、售后 | v MySQL业务数据 Redis缓存/分布式锁/设备状态模块边界上几个关键点一是设备接入服务和业务服务要分开部署不能混在一个进程里。原因是设备消息量一旦上来消息处理会拖垮业务接口而且设备接入服务的发布频率和业务服务完全不同混在一起会导致两边互相牵制。二是设备状态不落 MySQL全放 Redis。每台设备在线还是离线、当前处于什么工作模式、最后一次心跳时间这些数据用 Redis 的 hash expire 就能解决读写都是微秒级要是每次查设备状态都去查 MySQL设备规模到几百台就会明显吃力。三是指令下发和指令回执必须走异步链路。业务系统创建订单后把“出货指令”扔给消息队列或者直接通过 Broker 下发设备出货完成后上报结果业务系统再更新订单状态。整个过程不能同步等待否则用户那边 3 秒没响应支付接口都超时了。2. 通信协议与设备接入的核心细节2.1 Topic 规划与报文格式设计MQTT 里 Topic 的设计非常重要它直接影响消息隔离性和权限控制。我在这个项目里的 Topic 规划如下v1/{deviceId}/report # 设备上行状态上报、心跳、出货结果、故障 v1/{deviceId}/cmd # 服务端下行出货指令、重启指令、查询指令 v1/{deviceId}/ack # 设备上行指令回执 v1/broadcast/reboot # 服务端下行广播指令按需为什么这么设计因为 MQTT 支持通配符订阅运营后台只需要订阅v1//report就能拿到所有设备的上报消息然后按 deviceId 去分发到对应业务。权限控制也好做设备连接时限制它只能发布到/report和/ack不能发布到/cmd从协议层避免伪造指令。报文格式我选择的是 JSON而不是更省流量的 Protobuf 或二进制原因很实在开发效率高、排查问题方便、后面接手的同事容易看明白。无人售货机的消息频率并不高上报一条 JSON 也就几百字节4G 流量费完全可以忽略不计。如果是大规模传感器网络或者 NB-IoT 场景我会毫不犹豫换 Protobuf但售货机场景没必要。一条上报消息的格式大概是这样的{ msgId: UUID, type: REPORT, timestamp: 1710000000, deviceId: VM001, data: { status: IDLE, temperature: 5.2, doorOpen: false, faultCode: null, stockStatus: [ {aisle: 1, stock: 5}, {aisle: 2, stock: 0} ] } }这个设计里最关键的字段是msgId。它是一条消息的唯一 ID服务端拿到后用来做幂等去重避免网络重传导致业务重复执行。这一点在后面讲“幽灵出货”的时候会详细展开。2.2 设备鉴权是第一个大门槛设备接入第一个要解决的问题是服务端怎么知道连上来的是自己的售货机而不是别人伪造的客户端无人售货机涉及到真金白银的交易如果设备鉴权被绕过攻击者可以直接下发“免费出货”指令整个系统的信任基础就崩塌了。我在项目里用的是一机一密 HMAC 动态签名的方式。每台设备在出厂时烧录一个唯一标识deviceSecret相当于设备的密码这个密钥同时记录在服务端数据库中。设备连接 MQTT Broker 时username 填设备序列号password 不直接填密钥而是用当前时间戳和 deviceSecret 做 HMAC-SHA256 生成的动态签名。Broker 侧的认证插件拿到设备序列号和签名后去 Redis 里查这台设备的密钥验签通过才放行。// 设备端伪代码以 Java 的思维来描述这个过程 long timestamp System.currentTimeMillis() / 1000; String password hmacSha256(deviceSecret, deviceId timestamp); // 连接时 usernamedeviceId, passwordpassword, clientIddeviceId动态签名的好处是即使网络上有人抓包拿到了设备和 Broker 之间通信的握手信息他也没法重放因为签名里绑定了时间戳过期就无效。另外clientId要固定用 deviceId这样 Broker 能感知同一台设备的重复连接并踢掉旧连接防止一台设备被两个连接同时控制。2.3 消息幂等、时序与可靠性补偿设备的网络不可靠会导致三种情况消息丢了、消息重了、消息顺序乱了。这些不能靠设备端保证服务端必须做兜底。先讲幂等。拿“出货结果上报”来说设备可能因为网络抖动把同一条出货成功的消息发了两次。如果服务端不加判断库存就扣了两次订单状态也重复更新。我用的方案是双重保障MySQL 订单状态流转表加唯一索引order_id event_type入库前再用 RedisSETNX msgId做前置去重。两次过滤下来重复消息基本进不了业务逻辑。// 用 Redis 做幂等前置过滤 String key mq:msg:dedup: msgId; Boolean first redisTemplate.opsForValue().setIfAbsent(key, 1, Duration.ofMinutes(10)); if (!Boolean.TRUE.equals(first)) { log.warn(duplicate message ignored, msgId{}, msgId); return; }再讲时序。设备上报状态的同时服务端可能在下发出货指令两者没有严格先后。比如设备状态上报“出货成功”但对应的指令回执还没到服务端怎么处理我的处理方式是不依赖消息到达顺序而是把“指令状态”做成一张独立的表回执和上报都去更新这张表业务查询时以状态为准。最终一致性靠定时任务扫表兜底下单超过 30 秒还没有出货结果回执的自动标记异常进入人工核查或退款流程。3. 从扫码到出货完整链路与库存一致性3.1 下单-出货-回调的完整流程用户购买一瓶可乐背后完整的链路是这样的用户扫售货机上的二维码进入小程序/H5看到这台设备设备编号确定和它的商品列表。用户选中可乐发起支付走微信/支付宝统一下单。支付成功回调触发业务系统创建订单订单状态为“待出货”。业务系统调用库存服务预占库存不是直接扣减同时生成一条出货指令。出货指令经过 MQTT 发给设备设备控制对应货道电机出货。设备出货完成后上报结果成功 / 失败 / 卡货。业务系统根据结果更新订单状态出货成功扣减库存失败则触发退款。我把第 4 步做成预占库存而不是直接扣减原因很现实如果先扣库存但设备出货失败库存负数就对不上了。预占库存的意思是先把这瓶可乐“锁住”不让别人买走等出货成功再正式扣减出货失败就释放预占。3.2 出货防重与超时兜底出货指令下发之后最怕的是什么重复出货。比如指令在网络里卡了一下设备其实已经出货了但服务端没收到结果于是重试指令结果设备又吐了一瓶出来——用户只付了一瓶的钱机器出了两瓶这就是纯亏。我在这块的方案核心是“出货指令要幂等设备端要记住已执行过的指令”。具体两条腿走路第一服务端生成出货指令时带上唯一指令号cmdId。所有重试都必须复用同一个cmdId不能生成新指令号。设备端内存里维护一个最近已执行指令号的集合收到重复cmdId直接回“已执行”不会再次转动电机。第二服务端设置出货超时兜底。指令下发后如果 10 秒内没有收到回执不直接重发而是先向设备发送“查询出货状态”指令根据设备实际状态决定是补发还是标记异常。如果设备也查询不到结果比如设备断电了订单进入人工处理队列。有人可能问QoS 用 1 不就行了吗MQTT QoS 1 保证消息至少到达一次但网络中断的状态下Broker 和设备之间可能根本连不上QoS 也救不了。所以业务层的超时查询和状态对齐是必须的。3.3 库存扣减的最终一致性库存是这个系统里最容易出 bug 的地方。设备库存和数据库库存是两个世界的东西服务端数据库里有库存字段设备本地 Flash 里也有库存计数两边如果不同步就会出现“数据库显示有货机器实际空货道”或者反过来。我的方案是以“出货结果”为唯一依据来驱动库存变化用户支付成功时预占库存不修改真实库存。设备上报出货成功后扣减真实库存同时把设备上报的当前货道余量写回设备状态缓存。设备上报出货失败释放预占库存发起退款。定时任务每天凌晨做一次全量对账服务端向所有设备下发“上报库存”指令拿设备端库存和数据库库存做比对不一致的标记出来人工确认差异原因。这么设计之后数据库库存永远反映的是“已知成功出货后的剩余数量”而不是“预测数量”对账时才有依据。你如果让设备端上报一个数字就直接覆盖数据库那一次上报丢失就会造成库存永久不对。4. 中间件选型与硬件端适配4.1 Broker 选型EMQX 还是自研网关如果设备量在几千台以内直接用 EMQX别犹豫。开源版就能支持十万级连接有 Web 控制台、规则引擎和 API设备管理、消息轨迹都有省掉一大半 IoT 接入的活。Spring Boot 服务通过 MQTT 客户端我用的是 Eclipse Paho订阅主题或者通过 EMQX 的 HTTP API 发布消息。如果项目目标是彻底掌控通信链路、做高并发自研网关那 Netty 就是必须的。自己实现 MQTT 协议编解码处理 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ 这些报文好处是可以在网关层直接插入业务逻辑比如设备状态变更、指令拦截但代价是开发和维护成本高。我在实际项目里的做法是标准设备走 EMQX老款或私有协议设备走 Netty 网关两个接入层并存业务层不用关心设备是从哪个通道上来的。4.2 Redis 在这个系统里到底干了多少活我数了一下Redis 在这个项目里承担了至少七个职责设备在线状态与最后心跳时间缓存。设备动态签名的密钥缓存。消息幂等去重的前置过滤。预占库存的分布式锁。设备出货指令状态的短时缓存。热点商品库存的原子扣减用 Lua 脚本。补货单操作的数据一致性保护。几乎每个业务环节都有它。建议提前把 Redis 的键值设计和失效策略想清楚比如设备状态用hash存key 是device:status:{deviceId}field 是online、lastHeartbeat、fault等。别把所有乱七八糟的数据都塞进一个 key后面排查问题会非常痛苦。4.3 设备硬件怎么选主控方面售货机这种需要多路 IO 控制、稳定性和抗干扰能力要求高的场景STM32 是稳妥选择如果要做联网和本地简单逻辑ESP32 因为自带 WiFi/蓝牙非常适合原型验证和毕业设计。商用售货机一般还会有一个安卓板的触摸屏做主交互主控板负责货道控制两边通过串口通信。网络模组的选型上我强烈建议直接上 4G Cat.1 模组比如移远 EC200S 系列、合宙 Air724UG。Cat.1 功耗低、资费便宜而且覆盖了国内绝大多数物联网场景的 4G 网络。用 2G 模组会面临运营商逐步退网的问题用纯 WiFi 在商场景点是找死商场的 WiFi 环境复杂到会让你怀疑人生。设备端 MQTT 可以选的库比较多ESP32 上常见 PubSubClient轻量但功能弱、esp-mqtt官方库如果是蜂窝模组很多 AT 指令固件直接支持 MQTT 协议比如 EC200S 的 AT 指令里就有QMTOPEN和QMTPUB非常方便甚至不需要主控自己跑 MQTT 协议栈。5. 真实项目里踩过的坑与排查技巧5.1 断线重连、心跳风暴与指数退避售货机部署在商场后网络质量不稳定设备掉线是常态。第一个踩的坑是设备一掉线就疯狂重连还所有机器同时掉线导致 Broker 和接入服务同时被打爆——这就是“心跳风暴”。解决方案是指数退避 随机抖动。设备重连间隔从 1 秒开始每次失败翻倍1s、2s、4s、8s……最大到 5 分钟每次重连前叠加一个 0 到 30 秒的随机值防止大量设备同时重连。服务端的心跳踢除时间也不能设得太短不然设备网络抖动一下就掉线重连反而制造更多消息。另外要区分“网络心跳”和“业务心跳”。MQTT 协议层有 keepalive 机制但设备端可能正常连着网络业务程序已经卡死了。所以我让设备每隔 60 秒额外上报一条业务心跳带上当前状态服务端据此判断设备是否“活”着。连续 3 个业务心跳周期没收到才标记设备离线。5.2 消息乱序与“幽灵出货”有段时间运营反馈用户明明没买机器却掉了一瓶饮料出来。排查了很久发现是设备端有一条延迟了好几分钟的旧指令在用户新下单之后才刚被执行。旧指令带着旧的cmdId但因为设备端“已执行指令集合”容量设置太大、清理不及时这条旧指令又不在集合里于是被当成新指令执行了。这就是典型的消息乱序过期指令重放问题。我后来的修复方案有两个一是服务端在出货指令里增加下发时间戳设备端校验如果指令时间戳早于当前设备时间 5 分钟以上直接丢弃上报“过期”二是设备端的内存指令集合只保留最近 100 条且记录设备最新一次重启时间重启后清空集合避免旧指令在重启后继续生效。5.3 对账与日常巡检清单系统跑稳之后日常运维不能靠感觉得靠巡检脚本。我梳理了一套自动化巡检任务每天跑设备离线数量超过 5% 时触发告警。订单“已支付但未出货”超时未处理的数量大于 0触发告警。库存对账不一致的设备自动进入差异列表。设备上报故障码货道卡死、温控异常、门状态异常超过阈值通知运维人员现场处理。这些巡检任务本身也是 Java 的 Spring 定时任务放一个独立的 admin 服务里跑不跟业务服务混在一起避免定时任务影响线上接口性能。5.4 常见问题速查表现象可能原因排查思路设备一直显示离线网络模组信号差 / 心跳超时时间过短检查模组信号强度调整心跳踢除时间支付成功但不出货出货指令下发失败 / 设备死机查 Broker 消息轨迹设备端查看是否收到指令出货成功但订单未完成出货结果上报丢失扫订单状态表调用设备查询接口手动补单库存对不上出货成功消息重复/丢失 / 补货没有录入跑每日对账任务以设备上报为准消息延迟严重Broker 线程阻塞 / 业务处理慢看 EMQX 监控面板确认是否是消费端堆积同一设备卖了超过库存量的商品预占库存和真实库存逻辑混淆检查库存扣减是否只在“出货成功”回调执行6. 如果让我重做一遍我会这样做这套系统我从毕业设计一路做到商用部署最大的体会是技术难点不在任何一个单独的技术点上而在“可靠地把现实世界发生的事同步到数字世界里”。如果现在有人要复刻这个项目我的建议是先从最小闭环开始一台 ESP32 开发板、一个 EMQX、一个 Spring Boot 服务、一个最简单的下单页面。先打通“页面下单 → 服务端下发 → 设备出货 → 设备上报 → 订单完成”这条主链路再逐步加入库存、支付、鉴权、对账、告警。不要一上来就想把所有功能做全否则任何一个环节出问题你都分不清是设备的问题、网络的问题还是业务代码的问题。另外一个很实用的技巧是在开发阶段就给每个设备准备一个“模拟器”用 Java 写一个模拟设备端的程序能收发 MQTT 消息、能模拟出货成功/失败。这样调试服务端逻辑时不需要真实硬件也能跑通全部流程效率翻倍。我在项目里就是靠这个模拟器把支付、退款、库存对账这些核心逻辑在硬件到场之前就测完了。最后送你一句我踩坑踩出来的话无人售货系统里的“无人”两个字只意味着没有店员不意味着没有人维护系统。设备会坏、网络会抖、消息会丢、人会误操作把异常处理做好了这个系统才算真正完工。

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

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

免费获取报价