很多人看物联网项目,第一反应是"技术好多"。但每一项技术都不是为了炫技,而是被问题逼出来的。这篇把整套系统的关键技术选型讲清楚:为什么要用这些技术、不用会怎样、它们的核心原理是什么。读完你就能回答"为什么是这个组合"。
先看全景:这套系统用了什么
设备侧 ESP8266(硬件采集)
通信协议 自定义二进制协议(24 字节定长帧)
接入层 EMQX(MQTT Broker)+ Spring Integration MQTT(客户端)
推送层 Netty(WebSocket 长连接)
消息层 RabbitMQ(异步解耦)
存储层 Redis(实时快照)+ IoTDB(时序历史)+ MySQL(业务数据)
前端 uni-app 小程序 + Vue3 管理后台
下文按「通信层 → 消息与存储层」逐项解析,每项都回答四个问题:场景痛点、为什么不用别的、核心原理、落地与代价。
一、通信层
1. 为什么用 MQTT,而不是 HTTP 轮询 / WebSocket 直连设备?
场景痛点
充电桩每 10 秒上报一次状态,还要能随时接收云端下发的启停指令。设备是 4G 流量卡 + 小内存单片机(ESP8266)。
候选方案对比
| 方案 | 机制 | 致命问题 |
|---|---|---|
| HTTP 轮询 | 设备每 10s GET 一次服务器 | ① 服务器被轮询打爆(100 台桩 × 每天 8640 次请求)② 实时性差 ③ 4G 流量按 KB 计费,浪费严重 ④ 服务器无法主动下发指令 |
| WebSocket 直连 | 设备主动与服务器建长连接 | ① 协议重(握手 + 帧头开销大),ESP8266 内存吃紧 ② 设备侧断线重连逻辑要自己写 ③ 大规模设备(上千)连接管理复杂 |
| MQTT(本项目) | 设备只和 Broker 通信(发布/订阅) | ✅ 协议极轻(固定头仅 2 字节)✅ QoS 分级 ✅ 遗嘱消息 ✅ 天然支持设备↔云端双向 ✅ 断线自动重连 |
核心原理(必懂)
- 发布/订阅模型 :设备发布到
charge/stat主题,谁订阅谁收------设备不关心有多少个消费方,消费方也不关心设备在哪 - QoS 三级:0 最多一次(丢了不管)、1 至少一次(可能重复)、2 恰好一次(开销最大)。本项目用 QoS 1:充电状态可容忍重复,但不能丢
- 遗嘱消息(LWT):设备异常掉线时,Broker 代它广播一条"我离线了",云端能及时发现设备故障
落地与代价
落地:设备 publish charge/stat(QoS 1),mqtt-client 订阅解析。代价:MQTT 只解决"消息怎么传",不解决"报文长什么样"------所以还要有第 5 项的自定义二进制协议配合。
2. 为什么用 EMQX 当 Broker,不用自研或 Mosquitto?
场景痛点
需要一个稳定的 MQTT 服务器来承接设备连接与消息路由。
候选方案对比
| 方案 | 优点 | 问题 |
|---|---|---|
| 自研 Broker | 完全可控 | 订阅关系管理、QoS 状态机、遗嘱、断线重连、集群------全部要自己实现,工作量巨大且极易出错 |
| Mosquitto | 轻量开源 | 功能够用但管理界面弱、集群能力弱、规则引擎/数据桥接需要额外插件 |
| EMQX(本项目) | ✅ 开源社区版免费 ✅ Web 控制台可视化管理 ✅ 内置规则引擎(消息可直接桥接到 Kafka/DB)✅ 支持百万级连接集群 | 社区版有节点数/功能限制(本项目规模用不到) |
核心原理
EMQX 基于 Erlang/OTP 构建,天生擅长高并发连接管理。它的价值在于把"设备连接管理"这件事从业务代码里彻底剥离------你的后端只需要订阅主题,不用管设备在哪台 Broker 上。
落地与代价
落地:Docker 部署 EMQX,mqtt-client 通过 ws://emqx:8083 连接(MQTT over WebSocket)。代价:引入了一个新中间件,部署链路多一环。
3. 为什么用 Spring Integration MQTT + Paho,不用裸 Paho 客户端?
场景痛点
Java 侧要订阅 MQTT 主题、接收消息、发消息。最直接的做法是引入 Eclipse Paho 的 MqttClient 自己写。
候选方案对比
| 方案 | 优点 | 问题 |
|---|---|---|
| 裸 Paho(MqttClient) | 简单直接 | 连接生命周期要自己管理、断线重连要自己写、消息回调线程模型要自己设计、和 Spring 容器无集成 |
| Spring Integration MQTT(本项目) | ✅ 连接生命周期交给 Spring 管理 ✅ 通道抽象(Channel)方便加拦截器/转换器 ✅ 断线重连由 Paho + Spring 协同处理 ✅ 与 Boot 应用集成度高 | 学习曲线略陡(要理解 MessageChannel、适配器等概念) |
核心原理
Spring Integration 把"消息收发"抽象成管道:InboundChannelAdapter(入站适配器)负责订阅接收 → 转成内部 Message → 进入 Channel → 业务处理器消费;OutboundHandler 负责把内部消息发出去。你写的业务代码只跟 Message 打交道,不碰 MQTT 细节。
落地与代价
落地:FactoryBuilder 构建 MqttPahoClientFactory,InBoundSubscribe 配入站适配器订阅 charge/stat,OutBoundSend 配出站 handler。代价:多一层抽象,排障时要知道"消息卡在通道还是卡在 MQTT"。
4. 为什么用 Netty 做小程序长连接,不用 Spring WebSocket?
场景痛点
小程序需要实时接收充电状态(进度、功率、费用),而且要能随时下发 START/STOP/RESUME 指令------必须维持长连接。
候选方案对比
| 方案 | 机制 | 致命问题 |
|---|---|---|
| Spring WebSocket(基于 Servlet) | 一连接一线程 | ① 上万连接 = 上万线程,内存爆炸 ② 线程切换开销巨大 ③ 粘包/拆包、心跳都要基于 Servlet 容器能力受限 |
| 原生 Socket 自己写 | 完全手撸 NIO | ① Reactor 模型、ByteBuf 管理、编解码、心跳、优雅关闭全要自己实现,工程量巨大且容易有 bug |
| Netty(本项目) | 事件驱动 NIO + Reactor 模型 | ✅ 一个线程管上千连接(boss/worker 线程模型)✅ 内置粘包/拆包、空闲检测、编解码器 ✅ 背压处理 ✅ 社区成熟 |
核心原理(必懂)
- Reactor 线程模型 :
bossGroup(NioEventLoopGroup)负责接受新连接,workerGroup负责读写 IO------worker 线程数通常是 CPU 核数 × 2,远小于连接数 - ChannelPipeline:一条连接的处理逻辑被拆成一串 Handler(编解码 → 协议升级 → 心跳 → 业务),每个 Handler 只干一件事,可插拔
- 零拷贝与堆外内存:Netty 用 ByteBuf 直接操作堆外内存,减少 GC 压力和内存拷贝
落地与代价
落地:WebSocketServ 用双 EventLoopGroup 引导,Pipeline 装配 IdleStateHandler(60,0,90) → HTTP 编解码 → 聚合 → WebSocket 升级 → 业务 Handler。代价:对开发者要求高------Handler 顺序错乱、线程模型理解不到位,容易写出隐藏 bug。
5. 为什么用自定义二进制协议,不用 JSON / Protobuf?
场景痛点
设备每 10 秒上报 8 个字段(桩 ID、电量、功率、费用、进度、状态等),走 4G 流量。报文格式必须省流量、好解析。
候选方案对比
| 方案 | 优点 | 问题 |
|---|---|---|
| JSON | 可读性好 | ① 字段名+括号+逗号冗余,一条消息 ~150 字节 ② 解析耗 CPU(设备端小内存)③ 4G 流量费贵 |
| Protobuf | 体积小、性能高 | ① 引入 protoc 生成工具链 ② 需要 .proto 文件管理 ③ 本项目中曾用后被移除(链路简化) |
| 自定义二进制(本项目) | ✅ 24 字节定长帧,体积缩减 60%+ ✅ 按偏移量位运算直读,零解析开销 ✅ 无工具链依赖 | ① 可读性差,调试要 hex 工具 ② 字段变更要两端同步升级(靠版本号管理) |
核心原理
二进制定长帧 = 约定好偏移量:0-1 字节起始符、1-3 桩 ID、3-11 记录 ID......收发双方按同一张表读写字节。用大端序(网络字节序)避免字节序歧义,用累加和校验检测传输错误。
落地与代价
落地:ChargeStatData 实现 toBytes() / fromBytes() / fromHexString(),纯位运算编解码。代价:协议迭代 3 版才稳定(18→22→24 字节),深刻教训是版本号字段要第一天就留。
二、消息与存储层
6. 为什么用 RabbitMQ 做异步分发,不用 Kafka / Redis?
场景痛点
设备一条状态要同时做三件事:推给小程序(实时)、写 Redis(快照)、存 IoTDB(历史)。最初是同步串行调用三个 HTTP 接口------任何一个下游慢/挂,整条链路卡死。
候选方案对比
| 方案 | 优点 | 问题 |
|---|---|---|
| Kafka | 吞吐极高 | ① 引入 ZooKeeper/KRaft,运维重 ② 本项目每秒几条消息,大材小用 ③ 语义偏"日志流"而非"任务分发" |
| Redis List/PubSub | 轻量 | ① 无 TTL 精细管理、无死信概念 ② 持久化弱 ③ PubSub 消息不落地,消费者离线即丢 |
| RabbitMQ(本项目) | ✅ 交换机类型丰富(Fanout/Direct/Topic)✅ 队列 TTL、死信队列、手动 ACK 开箱即用 ✅ 轻量易部署 ✅ AMQP 语义完整 | 单机吞吐不如 Kafka(本项目规模无感) |
核心原理(必懂)
- 交换机(Exchange)决定消息怎么路由:Fanout 广播给所有绑定队列(本项目三路下游都要全量收);Topic 按通配符路由(下行指令用)
- TTL(消息存活时间):本项目三路队列 TTL 不同------推送 10s(超时无意义)、缓存 30s(覆盖重连窗口)、存储 60s(尽量写入),按业务时效差异化设计
- 手动 ACK:处理成功才确认,失败重回队列重试;配合死信队列防止毒药消息无限重投
落地与代价
落地:RabbitConfig 声明 Fanout 交换机 + 3 队列 + TTL,ChargeStatConsumers 三个 @RabbitListener 并行消费。代价:目前未配死信队列(毒药消息隐患),生产环境需补上。
7. 为什么用 Redis 存实时状态快照?
场景痛点
小程序 WebSocket 断线重连需要时间,重连期间界面不能空白------需要"最近一次状态"兜底。
为什么不直接用 MySQL / 内存 Map?
| 方案 | 问题 |
|---|---|
| MySQL | 每次读状态查库,延迟高(毫秒级 vs 微秒级),且高频写库压力大 |
| JVM 内存 Map | 重启即失、多实例不共享,且和业务代码耦合 |
| Redis(本项目) | ✅ 内存读写微秒级 ✅ TTL 自动过期 ✅ charge:stat:{id} 天然适配"最近一次状态"语义 ✅ 独立进程可多服务共享 |
核心原理
Redis 是单线程事件循环 + 内存数据结构,key 的 TTL(本项目 24 小时)到点自动删除,无需业务代码清理。用 String 类型存 JSON 状态快照,读用 GET,写用 SET + EXPIRE。
落地与代价
落地:mqtt-client 三路消费者中的 redis 队列写快照,小程序重连后先 GET /charge/stat/latest/{id} 兜底。代价:Redis 本身单点(未做主从/哨兵),生产要补高可用。
8. 为什么用 IoTDB 存历史数据,不用 MySQL / InfluxDB?
场景痛点
每台桩 10 秒上报一次,100 台桩一天 86 万条时间序列数据,要支持"近 7 天每小时平均功率"这类聚合查询。
候选方案对比
| 方案 | 机制 | 问题 |
|---|---|---|
| MySQL | 行式存储 + B+Tree 索引 | ① 高频写入时索引膨胀、写放大 ② 历史数据查询全表扫描慢 ③ 存储成本高 |
| InfluxDB | 列式时序库 | ① 开源版集群/降采样等能力受限 ② 国内文档少 ③ schema 建模与 IoTDB 风格不同 |
| IoTDB(本项目) | 列式压缩 + 时间分区 + 树形 schema | ✅ 写入性能高(顺序写 + 压缩)✅ 时间分区裁剪让范围查询快 ✅ 内置降采样聚合 ✅ 国产开源、中文文档全 |
核心原理(必懂)
- 时序数据特征:高频 + 时间戳 + 数值,写入多、按时间范围查询多、老数据基本不更新------专门为这个特征设计的库才高效
- 树形路径 :
root.yeseesion.charge.s{stationId}.r{recordId},路径即 schema,天然适合设备-测点层级 - 分区与压缩:按时间分区存储,查询只扫相关分区;列式存储对"同列数值"压缩率高
⚠️ 落地教训(重要)
本项目的路径设计把 chargeRecordId(每次充电不同的雪花 ID)放进了路径 → 时序 schema 无限膨胀 ,每笔订单新建 6 条时间序列。正确做法:recordId 应作为 tag/测点属性,路径只到桩级别 root.yeseesion.charge.s{stationId}。这个教训说明:时序库的建模(路径设计)比 MySQL 的表设计更需要前瞻性。
代价
引入 IoTDB 需要额外部署(端口 6667),SessionPool 连接管理要配置好(本项目 maxSize=200)。对业务方来说多一个存储系统要维护。
9. 为什么用 MySQL 存业务数据?
场景痛点
用户、钱包、订单、充电记录、充电站、告警------这些是强一致性、事务性的业务数据,与高频时序数据性质完全不同。
为什么不也放进 Redis / IoTDB?
| 方案 | 问题 |
|---|---|
| Redis | 数据不持久、无事务性、难做复杂关联查询 |
| IoTDB | 时序库没有事务、外键、复杂查询能力,不适合订单/钱包这种强一致场景 |
| MySQL(本项目) | ✅ 事务(ACID)✅ 关联查询 ✅ 生态成熟(MyBatis-Plus 直连)✅ 逻辑删除/乐观锁支持 |
核心原理
MySQL 负责"关系型业务数据",IoTDB 负责"时序监控数据",Redis 负责"实时快照"------三种数据性质不同,用三种存储,各取所长。这是本项目架构上最清晰的一个决策。
落地
- 10 张表(cs_user、cs_wallet、cs_order、cs_charge_record、cs_station、cs_station_price、cs_connector、cs_alarm、cs_wallet_log、cs_recharge_order),MyBatis-Plus 通用 CRUD + Lambda 条件构造器。
小结
回到开头那句话:技术选型不是堆名词,而是被问题逼出来的。
- 设备端资源受限 → 逼出 MQTT + 二进制协议
- 上万连接要撑住 → 逼出 Netty
- 同步串行会卡死 → 逼出 RabbitMQ
- 断线不能白屏 → 逼出 Redis 快照
- 86 万条/天写不进 MySQL → 逼出 IoTDB
- 订单钱包要强一致 → 留在 MySQL
理解了"每个选择背后的为什么",这套架构就不再是名词堆砌,而是一个自洽的整体。这也是后续 1-7 篇逐模块详解的认知基础------建议先读这篇选型篇,再进入 【从零搭建物联网智能充电桩系统】1、系统架构与两条数据链路。