ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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轮询最简单调试直观实时性差设备频繁请求浪费流量采集频率低、设备量少的 demoTCP 私有协议 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它挂在网关下面用自己的私有协议把数据聚到网关网关才有IPJava服务端看到的也只是网关的连接。所以业务层不要按传感器IP去存数据而应该按deviceSn归属到某个网关。换句话说Java服务端的设备表里核心标识是网关编号传感器只是网关上报数据里的一个测量点维度。如果某个大棚部署了10个温湿度传感器网关可以把它们聚合后一起上报也可以分多条报文上报但每条报文里的deviceSn始终是网关的。把这一层想清楚后面设计数据库和接口时就不会把IP当成主键来用。3. 工程骨架与数据库设计Java后端怎么搭才不会后期返工3.1 Maven工程结构与核心依赖单模块够用别一上来就微服务很多初学者一看到“系统设计”四个字就想着Spring Cloud、分布式、微服务。环境监测系统真的不需要。设备量在几十到几百台、数据量每天几百万条以内单模块Maven工程加清晰分包完全够源码还好交、好讲、好扩展。我一般按controller / service / mapper / netty / entity / config分包整个工程结构一眼能讲明白。下面是最小可运行的pom核心依赖注意版本号我留了占位不用Spring Boot直接管版本的依赖一定要按你自己环境的兼容版本填。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version${spring-boot.version}/version /parent dependencies !-- Web与WebSocket -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- Netty 4.x接入层用 -- dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId /dependency !-- MyBatis Plus 3.x注意与Spring Boot版本兼容 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency !-- MySQL 8.x 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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里必须带serverTimezoneAsia/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 ChannelInitializerSocketChannel() { 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 SimpleChannelInboundHandlerString { 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次insertMySQL大概率扛不住连接池也会被打满。所以入库必须批量。我一般用一个简单的缓冲容器攒够500条或5秒定时刷新一次二者先到先触发。Component public class ReportBatchBuffer { private final ListEnvData 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; } ListEnvData 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要继承ServiceImplEnvDataMapper, 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 MapString, 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 MapString, 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里也写死serverTimezoneAsia/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物联网项目。希望帮到你。本文还有配套的精品资源点击获取
返回列表