news 2026/10/7 14:36:22

Java物联网环境监测系统:Netty接入、批量入库与WebSocket实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java物联网环境监测系统:Netty接入、批量入库与WebSocket实战

简介:这套基于Java的物联网环境监测系统设计源码,面向物联网、软件工程方向的开发者和Java初学者,也适用于课程设计或毕业设计场景,解决环境数据的实时采集、传输、分析与可视化展示问题。压缩包约3.51MB,共43个文件,以15个Java源文件、14个XML配置文件、3个JAR依赖包为主体,另有properties、project等辅助文件;其中Java文件承载系统业务逻辑,XML用于参数与运行配置,JAR包提供数据库与日志等扩展支持。项目采用模块化设计,划分数据采集、数据处理、通信、用户界面等模块,通过传感器接入与中心数据库联动,可用于温度、湿度、空气质量等指标的持续监测。当前已有338人浏览或学习,目录结构清晰,可直接导入开发环境查看源码结构,并在此基础上扩展传感器类型、优化数据分析算法,适合作为物联网环境监测项目的实战参考与二次开发蓝本。

1. 基于Java语言的物联网环境监测系统设计源码:先搞清这套代码真正难在哪

做物联网环境监测系统,最容易翻车的往往不是传感器驱动,而是Java服务端这座“黑匣子”——十几个温湿度、PM2.5、CO2设备同时上报,数据怎么接、怎么存、怎么推,才是真正熬夜的地方。基于Java语言的物联网环境监测系统设计源码,解决的正是这条链路:设备接入、数据落库、实时展示、报警联动,它不只是一堆CRUD接口。

很多人把精力全花在STM32或ESP8266的采集代码上,觉得Java后端就是几张表加几个接口。等真拿设备联调才发现,粘包拆包、设备时钟漂移、重复上报导致数据翻倍、告警刷屏,每一个都能让人半夜爬起来改代码。这套源码适合物联网毕业设计、Java课程设计,也适合想从Java基础走向Netty和WebSocket的工程师照着改;看完你会发现,环境监测系统的核心不在“监测”,而在“稳定接住数据”。

2. 先想清楚数据往哪流:环境监测系统的分层架构与协议选型

2.1 物联网网关接入的三种方式:HTTP、TCP私有协议、MQTT怎么选

环境监测的数据流基本是同一条管道:传感器把温湿度、PM2.5、CO2交给物联网网关,网关通过无线或有线上报给Java服务端,服务端落库后再推给浏览器大屏。Java在这里扮演的角色是“接入层 + 业务层”二合一,这也是为什么很多物联网毕业设计偏爱Java——Netty处理高并发长连接,Spring Boot管业务,MyBatis Plus管数据库,每一步都有现成轮子。

但第一步就得做选择:设备到底用什么协议上来?

接入方式优点缺点适合场景
HTTP JSON轮询最简单,调试直观实时性差,设备频繁请求浪费流量采集频率低、设备量少的 demo
TCP 私有协议 + Netty实时性好,报文紧凑要自己处理粘包拆包、心跳网关设备数量稳定,毕业设计最常见
MQTT + EMQX量大、离线消息、主题灵活多引入一个中间件,运维成本高上千设备、需要断线补传的生产系统

我给这类环境监测系统做架构时,默认选第二种:TCP私有协议 + Netty。原因很实在:设备是自家网关,协议自己定,字段可以按需求精简;Netty在Java生态里足够稳,一个服务端能干到几千个连接,不用像MQTT那样再部署一套Broker。如果后面设备量真上来了,把接入层换成MQTT客户端即可,业务层代码不用动。

2.2 定义设备上报报文:一个字段都不能少的JSON标准

协议定了,接着就是把“设备上报的报文”长什么样写死。下面是我常用的报文格式,设备端把这条JSON通过TCP发到Java服务端,服务端用Netty解码后交给业务层处理。

{ "deviceSn": "A10001", "type": "ENV", "ts": 1735689600000, "seq": 101, "data": { "temperature": 25.6, "humidity": 46.8, "pm25": 35, "co2": 812 } }

这个报文里每个字段都有它的用途,不能只图省事塞几个数字。deviceSn是网关设备唯一编号,服务端按它区分是哪个机房、哪个大棚的数据;type用来区分环境数据、烟感数据、设备状态,为后续扩展留余地;ts是设备端毫秒时间戳,这点很多人会漏——如果不用设备时间戳,而是用服务端接收时间,那么设备断网后补传的数据全会挤在当前时刻,曲线直接失真;seq是设备端的自增序号,这是后面做幂等去重的关键;data里各字段单位必须固定,比如温度用摄氏度保留一位小数,PM2.5用整数,CO2用ppm,绝不能一会儿摄氏度一会儿华氏度。

我们在第4章会看到,Netty的LengthFieldBasedFrameDecoder要配合这个报文来解,所以设备端在发送时,需要在JSON前加4字节的长度头,Java端解析时才不会把两条报文粘在一起。这个坑后面专门讲。

2.3 网关、传感器、服务器三者的关系要摆正

热词里有个问题叫“物联网网关与传感器的IP关系”,其实很多刚做这个方向的人会在这里犯迷糊:传感器一般没有独立公网IP,它挂在网关下面,用自己的私有协议把数据聚到网关;网关才有IP,Java服务端看到的也只是网关的连接。所以业务层不要按传感器IP去存数据,而应该按deviceSn归属到某个网关。

换句话说,Java服务端的设备表里,核心标识是网关编号;传感器只是网关上报数据里的一个测量点维度。如果某个大棚部署了10个温湿度传感器,网关可以把它们聚合后一起上报,也可以分多条报文上报,但每条报文里的deviceSn始终是网关的。把这一层想清楚,后面设计数据库和接口时就不会把IP当成主键来用。

3. 工程骨架与数据库设计:Java后端怎么搭才不会后期返工

3.1 Maven工程结构与核心依赖:单模块够用,别一上来就微服务

很多初学者一看到“系统设计”四个字,就想着Spring Cloud、分布式、微服务。环境监测系统真的不需要。设备量在几十到几百台、数据量每天几百万条以内,单模块Maven工程加清晰分包完全够,源码还好交、好讲、好扩展。我一般按controller / service / mapper / netty / entity / config分包,整个工程结构一眼能讲明白。

下面是最小可运行的pom核心依赖,注意版本号我留了占位,不用Spring Boot直接管版本的依赖一定要按你自己环境的兼容版本填。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring-boot.version}</version> </parent> <dependencies> <!-- Web与WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Netty 4.x,接入层用 --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> <!-- MyBatis Plus 3.x,注意与Spring Boot版本兼容 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- MySQL 8.x 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

逻辑说明:Spring Boot的parent帮你管住Web、Netty、MySQL驱动的版本,避免自己挑版本挑到互相打架;MyBatis Plus不在Spring Boot的依赖管理里,所以要显式给版本。这里我用${mybatis-plus.version}占位,是提醒你下载源码后第一件事就是检查这个版本和你用的Spring Boot对不对得上,Spring Boot 2.7配MyBatis Plus 3.5.x是常见组合,Spring Boot 3.x则要换mybatis-plus-spring-boot3-starter。

3.2 application.yml里最容易忽视的5个参数

配置文件决定了服务端能不能稳定跑,不是写个端口和数据库地址就完事。下面这份yml我标注了每个参数在这个场景里的实际作用。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/env_monitor?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto app: netty: port: 8081 boss-threads: 1 worker-threads: 4 batch: threshold: 500 flush-interval-ms: 5000 timeout: device-offline-ms: 120000

参数说明:数据库连接URL里必须带serverTimezone=Asia/Shanghai,否则MySQL 8会按UTC处理时间,导致入库时间比本地慢8小时;hikari.maximum-pool-size在设备量100台、每10秒上报一次时,20个连接足够,调太大会白白占用数据库资源;Jackson的时区设置影响JSON序列化,前端大屏看到的告警时间才会和本地一致;app.netty.port是设备接入端口,与server.port分开,浏览器走8080,设备走8081,物理隔离更清晰;app.timeout.device-offline-ms用于判断设备离线,超过2分钟没收到任何报文就标记下线。

3.3 核心表结构设计:设备表、采集记录表、报警规则表

数据库是这套源码里最不能偷懒的部分。环境监测的典型表不多,三张必建:设备表、环境采集记录表、报警规则表。以下SQL是MySQL 8版本。

CREATE TABLE device ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_sn VARCHAR(64) NOT NULL, location VARCHAR(128), status TINYINT DEFAULT 1 COMMENT '1在线 0离线', last_report_time BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_sn (device_sn) ); CREATE TABLE env_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_sn VARCHAR(64) NOT NULL, temperature DECIMAL(5,1), humidity DECIMAL(5,1), pm25 INT, co2 INT, report_time BIGINT NOT NULL, seq BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_device_seq (device_sn, seq), KEY idx_report_time (report_time) ); CREATE TABLE alarm_rule ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_sn VARCHAR(64) NOT NULL, metric VARCHAR(32) NOT NULL, max_value DECIMAL(10,2), min_value DECIMAL(10,2), continuous_times INT DEFAULT 3, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

设计要点:env_data的report_time我用BIGINT存毫秒时间戳,而不是DATETIME,原因很直接——设备上报的时间戳本来就是毫秒值,直接落库省一次转换,排序比较也快;要展示成“yyyy-MM-dd HH:mm:ss”时,交给前端或SQL的FROM_UNIXTIME处理就行。uk_device_seq唯一索引是幂等去重的最后一道防线,即使业务层漏了判断,数据库也会把重复数据挡在门外。idx_report_time建在报表查询最常用的时间字段上,否则数据量超过几十万条后,按时间拉曲线的SQL会慢到让你怀疑人生。

3.4 用MyBatis Plus实体类生成建表SQL:反射拼接DDL的思路

“MyBatis Plus根据Java实体类生成创建表的SQL语句”是很多人搜过的需求。先说结论:MyBatis Plus没有内置的autoDdl开关,官方也不建议在生产环境自动建表。常见的做法是写一个小工具,利用MyBatis Plus的TableInfoHelper反射出实体类的字段信息,再拼接成DDL语句。这个工具用来生成初版表结构、跟设备端联调时重建测试库都很方便。

下面是一个简化可跑的示例:

public class DdlGenerator { public static String generateCreateTableSql(Class<?> clazz) { TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE IF NOT EXISTS ") .append(tableInfo.getTableName()).append(" (\n"); // 主键 sb.append(" ").append(tableInfo.getKeyColumn()) .append(" BIGINT AUTO_INCREMENT PRIMARY KEY"); // 普通字段 for (TableFieldInfo fieldInfo : tableInfo.getFieldList()) { sb.append(",\n ").append(fieldInfo.getColumn()) .append(" ").append(mapJavaTypeToDbType(fieldInfo.getPropertyType())); } sb.append("\n);"); return sb.toString(); } private static String mapJavaTypeToDbType(Class<?> type) { if (type == String.class) return "VARCHAR(128)"; if (type == Long.class || type == long.class) return "BIGINT"; if (type == Integer.class || type == int.class) return "INT"; if (type == BigDecimal.class) return "DECIMAL(10,2)"; if (type == LocalDateTime.class) return "DATETIME"; return "VARCHAR(64)"; } }

逻辑说明:TableInfoHelper.getTableInfo()是MyBatis Plus 3.x提供的反射入口,getTableName()拿到@TableName注解的表名,getKeyColumn()拿到主键列名,getFieldList()拿到所有普通字段。mapJavaTypeToDbType()只做了极简的类型映射,实际用的时候要补上LocalDate、Boolean等类型。生成出来的SQL一定人工复核一遍,特别是字段长度和精度,这个工具只能减少手写量,不能保证完全符合你的业务需求。

4. 核心链路实现:Netty接入、批量入库、WebSocket推送与报警联动

4.1 用Netty在设备接入层落地:ServerBootstrap与粘包拆包

接入层是整个环境监测系统的“大门”,设备每秒都在往里挤数据,大门不能堵。Netty服务端的启动代码几乎是固定的套路,但有几个参数必须按你的报文来调。下面是可直接借鉴的ServerBootstrap配置。

EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(4); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { // 4字节长度头 + JSON正文 ch.pipeline().addLast(new LengthFieldBasedFrameDecoder( 1024 * 1024, 0, 4, 0, 4)); ch.pipeline().addLast(new StringDecoder(StandardCharsets.UTF_8)); ch.pipeline().addLast(new EnvDataHandler()); } }); ChannelFuture future = bootstrap.bind(8081).sync(); future.channel().closeFuture().sync();

参数说明:LengthFieldBasedFrameDecoder这五个参数分别是最大帧长度、长度字段偏移、长度字段字节数、长度调整值、剥离字节数。这里约定设备报文的前4个字节是大端序的报文长度,长度字段本身不算在正文里,所以要剥离4字节。maxFrameLength设成1MB,防止设备异常时发超大包把内存撑爆。解码器后面跟StringDecoder,把ByteBuf转成完整JSON字符串,再交给EnvDataHandler处理。

EnvDataHandler里最忌讳的事,是在Netty的IO线程里直接查数据库。正确姿势是把解析出的数据丢给业务线程池:

public class EnvDataHandler extends SimpleChannelInboundHandler<String> { private final EnvDataService envDataService; @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { EnvReport report = JSONUtil.toBean(msg, EnvReport.class); if (!checkDeviceSn(report.getDeviceSn())) { ctx.writeAndFlush("{\"code\":401,\"msg\":\"device not found\"}"); return; } // 丢给业务线程池,避免阻塞Netty worker BusinessExecutor.execute(() -> envDataService.saveReport(report)); } }

这样做的原因很直接:Netty的worker线程是处理网络事件的,一旦在channelRead0里做慢操作,比如一次数据库insert耗时50ms,那么这一个线程上所有连接的读写都会被拖住。设备多起来以后,现象就是连接时不时断开、数据延迟突然变高。

4.2 批量入库:不能来一条插一条

环境监测的数据特征是高频、单条小。一台设备10秒上报一次,100台设备每秒也就10条,单条插入看似能跑。可如果是1000台、1秒上报一次,每秒就是1000次insert,MySQL大概率扛不住,连接池也会被打满。所以入库必须批量。

我一般用一个简单的缓冲容器,攒够500条或5秒定时刷新一次,二者先到先触发。

@Component public class ReportBatchBuffer { private final List<EnvData> buffer = new ArrayList<>(); public synchronized void add(EnvData data) { buffer.add(data); if (buffer.size() >= 500) { flush(); } } public synchronized void flush() { if (buffer.isEmpty()) { return; } List<EnvData> batch = new ArrayList<>(buffer); buffer.clear(); // 批量插入,注意Service要继承ServiceImpl才有saveBatch envDataService.saveBatch(batch, 500); } }

逻辑说明:add里做两个判断——数据量到了500条立即刷新;同时另起一个@Scheduled(fixedDelay = 5000)定时任务每5秒调一次flush,兜底处理那些没攒够500条但也不能拖太久的数据。saveBatch(batch, 500)的第二个参数是批次大小,MyBatis Plus会把这个大List再拆成500条一批执行,避免一条SQL过长超出数据库的max_allowed_packet限制。

有一点要注意:saveBatch走的是MyBatis Plus的ServiceImpl,所以你的EnvDataServiceImpl要继承ServiceImpl<EnvDataMapper, EnvData>。批量插入的SQL在生产环境通常还会优化成INSERT INTO ... VALUES (...), (...), (...),但毕业设计阶段用saveBatch就够了。

4.3 WebSocket推送实时数据:前端大屏的数据通道

数据库落库完成只是把数据“存住了”,用户要看实时曲线,还需要一条从服务端到浏览器的推送通道。WebSocket比定时轮询HTTP接口省流量、延迟低,浏览器端拿到的就是毫秒级的实时数据。在Spring Boot里,用@ServerEndpoint实现最直接。

@Component @ServerEndpoint("/ws/env/{deviceSn}") public class EnvWebSocket { private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("deviceSn") String deviceSn) { SESSION_MAP.put(deviceSn, session); } @OnClose public void onClose(Session session) { SESSION_MAP.values().remove(session); } @OnMessage public void onMessage(String message, Session session) { // 浏览器心跳,这里可以更新时间戳 session.getBasicRemote().sendText("pong"); } public static void pushToAll(String json) { for (Session session : SESSION_MAP.values()) { if (session.isOpen()) { session.getAsyncRemote().sendText(json); } } } }

逻辑说明:容器用ConcurrentHashMap以deviceSn为key,一个设备一条连接,这样服务端收到新数据后能精准推给关心该设备的页面,而不是全局广播。pushToAll里用了getAsyncRemote().sendText,异步发送不阻塞当前线程;如果某条连接已经掉线,isOpen()会判断出来,发送前跳过即可。

关于心跳:浏览器端需要每隔30秒发一条消息,服务端在onMessage里更新最后活跃时间;服务端另起一个定时任务,每60秒扫一遍超时的Session并关闭。否则客户端断网不关页面,服务端这条连接会一直挂着占资源,时间久了连接数虚高。

4.4 报警规则引擎:连续越限才告警,恢复后通知

环境监测系统不带报警,价值就少了一半。但报警逻辑不能只是“超限就发”,否则传感器一个瞬时毛刺就能把用户手机打爆。我这里的规则比较朴素:同一个设备,连续N次上报越限才触发告警,N由alarm_rule表的continuous_times字段决定。

public class AlarmEvaluator { private final Map<String, Integer> exceedCountMap = new ConcurrentHashMap<>(); public boolean evaluate(EnvData data, AlarmRule rule) { boolean exceed = data.getPm25() > rule.getMaxValue(); String key = rule.getDeviceSn() + ":" + rule.getMetric(); if (exceed) { int count = exceedCountMap.merge(key, 1, Integer::sum); if (count >= rule.getContinuousTimes()) { // 触发告警后清零计数,防止重复报警 exceedCountMap.put(key, 0); return true; } } else { exceedCountMap.put(key, 0); } return false; } }

逻辑说明:merge(key, 1, Integer::sum)是原子操作,多线程同时上报同一个设备的数据时,计数不会丢。连续越限达到N次后返回true,业务层此时去记录一条报警记录并推送WebSocket消息。这里有一个常见做法是,告警触发后还要再叠加一个“冷却时间”,比如30分钟内不再重复告警,即使再次越限也不发,只有恢复后下一次越限才重新开始计数。报警恢复通知也很重要——设备恢复正常时,要发一条“已恢复”的消息,不然用户永远不知道问题是否解除了。

5. 常见问题排查:物联网环境监测系统最容易被数据打脸的5个坑

5.1 凌晨曲线比白天还高:设备时钟漂移与时区错乱

现象:数据库里凌晨1点的温度数据比下午2点还高,曲线完全对不上时间轴;或者是所有数据都集中在当前时间,历史曲线变成一条竖线。

原因:两个问题叠加。一是设备端的时钟没校准,设备重启后RTC回到出厂时间,上报的ts就不准了;二是服务端连接MySQL时没指定时区,或者代码里把设备毫秒时间戳转成LocalDateTime时用了默认时区,导致所有时间整体偏移8小时。

解决:服务端在解析报文时,先做时间偏差校验——拿ts和设备当前最新的一次report_time对比,如果两个时间差超过5分钟,直接拒收并打日志。这样即使设备时钟漂移,系统也不会收一堆脏数据。同时,服务端自身统一用Asia/Shanghai时区,数据库连接URL里也写死serverTimezone=Asia/Shanghai,不让任何一环走系统默认时区。

5.2 数据库记录莫名翻倍:粘包、重传与重复上报

现象:设备实际采集了100条数据,数据库里却查出来200条,而且report_time几乎一模一样。

原因:第一层是TCP粘包拆包没处理好,两条报文被解码器当成一条,或者一条被拆成两半,解析出脏数据;第二层是设备端网络抖动时TCP重传,服务端处理了两次;第三层是设备逻辑里没做幂等,重连后把最近几次数据重新上报一遍。

解决:Netty端用LengthFieldBasedFrameDecoder把报文边界切开,这是第一道防线;业务层入库前根据deviceSn + seq查一次唯一索引,这是第二道防线;数据库里建UNIQUE KEY uk_device_seq (device_sn, seq),这是最后一道防线。三道全上,重复数据基本进不了库。

5.3 PM2.5突然冲到999:传感器毛刺与数据滤波

现象:监测曲线平时PM2.5稳定在30-50之间,偶尔某一秒突然跳到999,过一秒又恢复。用户截图投诉,说系统坏了。

原因:传感器本身受电磁干扰、灰尘遮挡或瞬时风速影响,偶尔会输出一个离谱的异常值。这个值不是设备坏了,也不是网络问题,但如果不处理,会直接污染统计报表,甚至触发误报警。

解决:服务端入库前加一道滑动窗口滤波,最简单的做法是取最近3次上报的中位值,而不是直接用最新值。比如最近三次是35、42、999,排序后取中位数42,毛刺就被过滤掉了。更省事的做法是设一个“单次变化上限”,比如PM2.5单次变化超过50就认为无效,沿用上一次值。这个阈值要根据你的传感器型号和现场环境去调,是典型的“参数玄学”,但值得花时间测一组合理值。

5.4 告警风暴:一小时发了几百条报警短信

现象:某仓库CO2浓度超限,用户一个小时收到几百条“二氧化碳超限”短信和推送,手机通知栏被刷屏,最后直接卸载了App。

原因:报警逻辑只判断了“是否越限”,没有判断“是否已经报过”。设备每10秒上报一次,每次超限都触发告警,一小时就是360条。

解决:报警触发后进入冷却期,比如30分钟内同一设备同一指标不再重复告警,只在恢复时发一条恢复通知。冷却期结束后如果仍处于越限状态,再发新一条告警。这样既不会漏报,也不会刷屏。把这段逻辑加进报警引擎里,比调报警阈值重要得多。

5.5 服务假死,连接池被打满:数据库成了瓶颈

现象:设备连接正常,WebSocket也正常,但页面加载数据卡死,日志里全是“connection is not available, request timed out after 30000ms”。

原因:代码里每收到一条报文就插入一次数据库,高峰期MySQL连接池的20个连接全被占用,一个个排队等事务提交。加上有些事务里还夹着报警查询,事务时间被拉长,连接释放更慢。

解决:按第4.2节的方式改成批量入库,事务只包住批量insert那一下;报警判断放在入库的同一批次里做,不要单独开事务查库。另外检查是不是有慢SQL,用EXPLAIN看env_data表的查询有没有走idx_report_time索引。这个坑解决完,连接池占用率通常能降一半以上。

6. 进阶:断点续传与上线前压测,把系统从“能跑”推到“敢跑”

6.1 设备离线补传:时间戳对齐才是后悔药

生产环境里设备会离线,可能是网关断电,也可能是网络抖动。设备端要做的,是把离线期间没发出去的数据存进本地Flash,恢复连接后按时间顺序补传。服务端收到补传数据时,入库用的时间必须是设备原始ts,而不是接收时间,否则所有离线数据会堆在恢复时刻,历史曲线直接失真。

补传数据量可能很大,比如离线两小时、10秒一条就是720条。服务端不要按实时数据的逻辑逐个处理,而是把补传数据批量写入env_data表,deviceSn + seq唯一索引会自动把中途重传的重复数据挡掉。需要注意的是,补传期间不要触发报警,因为那是历史数据,报警逻辑只对实时上报的数据生效。

6.2 上线前压测:用模拟器把系统跑出原形

Java服务端不能等到真设备进场再验证,到那时发现问题已经晚了。我习惯在联调前写一个模拟器,用Netty或Socket批量建连接,按真实频率上报模拟报文,把服务端跑半小时以上,重点看几个指标。以下是我常用的验证清单。

检查项压测方法合格线
连接稳定性1000个模拟连接保持12小时掉线率低于0.1%
入库吞吐量每秒500条上报持续30分钟数据库积压数为0
推送延迟服务端记录WebSocket收到消息的耗时P95小于500ms
重复数据压测后统计env_data总行数与模拟器发送条数偏差小于0.1%

压测时重点关注两个容易翻车的地方:一是Netty的worker-threads配太小,CPU没跑满但数据积压;二是批量入库的threshold配太大,内存里积压太多数据。这两个参数要按压测结果来回调,没有一劳永逸的“最佳值”。

我在做这类系统时吃过最大的教训,是第一次联调真设备时没做时钟校准,夜里两点大棚的加温数据全堆在同一分钟,曲线直接崩掉,用户半夜打电话来质问。后来我把“时间对齐”和“幂等去重”当作和数据库一样重要的基础设计,而不是上线后再补的功能。环境监测系统要交付的不是“能跑通”,是“敢上线”;以上这套链路完整走一遍,你拿到的就是一个能扛住真实数据、能解释清楚每个参数来由的Java物联网项目。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 14:36:16

KNX有线智能家居:HomeAssistant驱动的确定性家居控制系统

1. 这不是“装个APP就能用”的智能家居&#xff0c;而是用铜线扎进墙里、十年不换的硬核基建你有没有过这样的体验&#xff1a;早上起床&#xff0c;手机点开APP&#xff0c;等三秒——窗帘没动&#xff1b;再点一次&#xff0c;灯亮了&#xff0c;但空调温度还没同步&#xff…

作者头像 李华
网站建设 2026/10/7 14:36:13

MOSFET开关损耗全解析:原理、计算、实测与优化策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 14:33:53

caveman模式:在终端用自然语言指挥AI写代码的工程实践

“caveman”最近在技术群里反复出现&#xff0c;一开始我以为又是什么新的段子梗&#xff0c;直到看到有人在讨论用它直接在终端里改代码&#xff0c;我才意识到这个词背后其实藏着一整套值得聊的东西。如果你只是把它当成一个普通工具的名字&#xff0c;那你看不到它真正有意思…

作者头像 李华
网站建设 2026/10/7 14:32:52

AutoDL 与 Trae 连接实战:用 TaoToken 统一 Key 打通 SSH 远程开发链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华