简介:本资源是一套基于SpringBoot构建的物联网数据采集系统服务器端完整源码,面向Java后端开发者及物联网应用学习者,解决高并发传感器数据接入、分布式缓存与集群部署等典型工业场景问题。压缩包共94个文件,含48个核心Java业务类(涵盖Gateway、Sensor管理及异步任务调度)、25个Thymeleaf前端页面、8个XML配置文件、5个JS交互脚本及1个application.yml主配置文件,整体仅644KB,轻量易部署。已有466人学习下载,适合中高级开发者深入理解IoT后端架构设计。读者可直接运行并掌握Redis多级缓存策略(查询缓存、数据队列缓存、分布式Session)、基于线程池的异步落库机制、Nginx+Tomcat集群联调方法,以及SpringBoot简化配置与内置容器优势在实际项目中的落地实践。
1. 为什么用 SpringBoot 搭建物联网数据采集服务器端,不是“选它”,而是“绕不开它”
你手上有几十台温湿度传感器、PLC 控制器、LoRa 网关,它们每 5 秒发一次 JSON 包,格式不统一、心跳间隔不一致、偶尔断连重传、还夹杂着乱码和空字段——这时候你打开 IDEA,新建一个普通 Java Web 项目,配 Tomcat、写 Servlet、手动解析 HTTP/HTTPS 请求、自己做线程池管理连接、硬编码处理设备注册鉴权……三天后你会发现:服务跑起来了,但第 4 台设备上线就 OOM,第 7 次断网重连后数据开始丢包,第 12 小时日志里全是java.net.SocketException: Connection reset。这不是玄学,是没踩对技术栈的典型翻车现场。
SpringBoot 不是“又一个框架”,它是把物联网数据采集服务器端从“黑匣子运维”拉回“可配置、可监控、可灰度”的关键支点。它自带嵌入式 Tomcat(省掉容器部署)、自动装配 Starter(比如spring-boot-starter-webflux支持百万级长连接)、健康检查端点(/actuator/health实时看设备在线数)、配置中心集成能力(YAML 一键切测试/生产环境设备白名单),更重要的是——它让「协议适配层」和「业务逻辑层」真正解耦。你不用再为每个新接入的 Modbus TCP 设备重写一遍 Socket 解包逻辑,而是把解码器塞进@Component,把设备路由规则写进@ConfigurationProperties,把告警策略抽成@Service方法。这套结构,正是当前主流物联网平台(如 ThingsBoard 社区版、华为 IoTDA 轻量 SDK 接入层)背后共用的底座逻辑。如果你正在做毕业设计、工业边缘网关对接、或中小制造企业的设备上云 PoC,这个源码工程不是“参考”,而是你跳过前 3 个月踩坑周期的后悔药。
2. 从零启动:用 SpringBoot 3.2 + Maven 搭出可运行的数据采集骨架
2.1 初始化最小可行工程:选对版本,避开 JDK 21 兼容雷区
SpringBoot 版本选择不是越新越好。当前(2024 年中)生产级物联网采集服务最稳组合是SpringBoot 3.2.x + JDK 17。别碰 3.3+(WebFlux 对 Netty 4.1.100+ 的 TLS 1.3 握手有已知 handshake timeout 问题),也别用 2.7(已 EOL,不支持 Jakarta EE 9+,后续接入 MQTT 5.0 或 WebSocket Subprotocol 会卡死)。我们用官方推荐方式初始化:
# 使用 Spring Initializr CLI(比网页生成更可控) curl https://start.spring.io/starter.tgz \ -d dependencies=web,validation,actuator,lombok,data-jpa,h2 \ -d javaVersion=17 \ -d bootVersion=3.2.8 \ -d baseDir=iot-collector-server | tar -xzvf -提示:
h2是内嵌数据库,仅用于快速验证设备元数据存储;真实部署必须替换为 PostgreSQL 或 MySQL(见 4.2 节)。actuator是必选项——没有它,你根本没法知道当前有多少设备 TCP 连接活着、HTTP 请求平均延迟多少毫秒。
解压后进入目录,确认pom.xml中关键依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.8</version> <!-- 严格锁定 --> <relativePath/> </parent> <properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>2.2 定义设备接入协议抽象层:HTTP + WebSocket + TCP 三通道并存
物联网设备协议五花八门:老旧 PLC 走 HTTP POST 带时间戳参数;新装 LoRa 网关用 WebSocket 心跳保活;产线机器人控制器坚持用原生 TCP Socket 发二进制帧。SpringBoot 不强制你只用一种方式——而是让你在同一工程里并行支撑。核心设计是协议适配器模式:
HttpDeviceController.java:处理/api/v1/device/{id}/data标准 REST 接口,校验X-Device-KeyHeader 防伪造;WebSocketDeviceHandler.java:继承TextWebSocketHandler,重写afterConnectionEstablished()记录设备 ID 到ConcurrentHashMap<String, WebSocketSession>;TcpDeviceServer.java:用Netty(通过spring-boot-starter-reactor-netty)启动独立 TCP Server,监听0.0.0.0:8081,每个连接分配DeviceChannelHandler解析自定义二进制协议头。
关键代码片段(TCP 通道):
@Component public class TcpDeviceServer { private final EventLoopGroup bossGroup = new EpollEventLoopGroup(1); // Linux 专用高性能组 private final EventLoopGroup workerGroup = new EpollEventLoopGroup(); @PostConstruct public void start() throws Exception { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(EpollServerSocketChannel.class) // 比 NioServerSocketChannel 性能高 30% .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2)); // 解析前2字节为长度 ch.pipeline().addLast(new DeviceChannelHandler()); // 自定义解码+业务处理 } }); bootstrap.bind(8081).sync(); } }参数说明:
LengthFieldBasedFrameDecoder是 Netty 提供的粘包拆包神器,0,2,0,2表示“长度字段在报文开头、占2字节、长度字段本身不包含在长度内、跳过前0字节读取长度”。这是处理 TCP 二进制协议的黄金参数组合,错过它,90% 的初学者会在设备发连续包时直接收到乱码。
2.3 设备元数据模型:用 JPA 实体类承载动态属性与生命周期
设备不是静态对象。一台网关今天连 5 个传感器,明天可能扩容到 20 个;温度探头要记录校准时间,PLC 需要绑定所属产线工位。所以DeviceEntity必须支持扩展字段:
@Entity @Table(name = "t_device") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DeviceEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String deviceId; // 设备唯一标识,如 MAC 或 SN @Column(nullable = false) private String protocol; // http / websocket / tcp @Column(columnDefinition = "jsonb") // PostgreSQL 原生 JSONB 类型 private String attributes; // {"location":"A3F-01","calibration_date":"2024-06-01"} @Column(name = "last_heartbeat", columnDefinition = "TIMESTAMP WITH TIME ZONE") private OffsetDateTime lastHeartbeat; @Column(name = "online_status") private Boolean onlineStatus = false; @CreatedDate @Column(updatable = false) private OffsetDateTime createdAt; }注意:
columnDefinition = "jsonb"仅对 PostgreSQL 有效。若用 MySQL,需改为columnDefinition = "json"并确保 MySQL 版本 ≥ 5.7。H2 仅用于开发,其JSON类型不支持索引,切勿在 H2 上测试大数据量查询性能。
启动应用后访问http://localhost:8080/actuator/health,返回{"status":"UP"}即骨架就绪。此时你已有:
- HTTP 接口接收标准 JSON 数据;
- WebSocket 通道维持长连接;
- TCP Server 解析二进制帧;
- 设备状态持久化到数据库;
- 全链路健康检查暴露。
3. 数据落地与实时分发:从原始报文到可查、可算、可告警
3.1 统一数据接入门面:DeviceDataReceiver 作为协议无关入口
所有协议通道最终都要把数据喂给同一个处理引擎。我们设计DeviceDataReceiver作为门面类,它不关心数据从哪来,只专注三件事:校验、标准化、路由。
@Service public class DeviceDataReceiver { // 注入不同协议的处理器,由 Spring 自动装配 private final List<DeviceDataHandler> handlers; public DeviceDataReceiver(List<DeviceDataHandler> handlers) { this.handlers = handlers; } public void receive(DeviceDataPacket packet) { // 1. 设备合法性校验(查 DB 白名单 + 时间窗口防重放) if (!deviceValidator.isValid(packet.getDeviceId(), packet.getTimestamp())) { log.warn("Invalid device or replay attack: {}", packet.getDeviceId()); return; } // 2. 标准化为统一 POJO(无论 HTTP JSON 还是 TCP 二进制,都转成 DeviceStandardData) DeviceStandardData standard = dataNormalizer.normalize(packet); // 3. 异步分发:存库 + 推 Kafka + 触发规则引擎 CompletableFuture.allOf( saveToDatabase(standard), pushToKafka(standard), triggerRuleEngine(standard) ).join(); } }DeviceStandardData是核心数据契约:
@Data @Builder public class DeviceStandardData { private String deviceId; private String sensorType; // temperature / humidity / vibration private Double value; private OffsetDateTime timestamp; private Map<String, Object> rawPayload; // 原始未解析字段,供溯源 private String locationTag; // 从 device.attributes 提取的物理位置 }3.2 存储选型实战:H2 开发 → PostgreSQL 生产,迁移脚本怎么写
开发阶段用 H2 极快,但上线必须换 PostgreSQL。迁移不是简单改application.yml,关键在Schema 初始化与数据迁移:
- 在
src/main/resources/application-prod.yml中配置:
spring: datasource: url: jdbc:postgresql://pg-server:5432/iot_collector username: iot_app password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate # 生产环境严禁 use 'update'! properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true- 创建
V1__init_schema.sql(Flyway 管理):
CREATE TABLE t_device ( id BIGSERIAL PRIMARY KEY, device_id VARCHAR(64) UNIQUE NOT NULL, protocol VARCHAR(20) NOT NULL, attributes JSONB DEFAULT '{}'::jsonb, last_heartbeat TIMESTAMPTZ, online_status BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_device_online ON t_device(online_status); CREATE INDEX idx_device_last_heartbeat ON t_device(last_heartbeat);- 数据迁移脚本
V2__migrate_h2_to_pg.sql(仅首次部署执行):
-- H2 导出为 CSV(开发机执行) SELECT * FROM t_device INTO OUTFILE '/tmp/device_backup.csv' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'; -- PostgreSQL 导入(生产机执行) COPY t_device FROM '/tmp/device_backup.csv' WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');提示:
ddl-auto: validate是血泪经验。曾有团队在生产环境误设update,导致某次升级后 Hibernate 自动删了t_device表的jsonb字段,所有设备属性丢失。validate会校验实体类与 DB Schema 是否一致,不一致直接启动失败,逼你手动写 Migration。
3.3 实时分发到 Kafka:为什么不用 RabbitMQ?吞吐量实测对比
物联网采集是典型的高吞吐、低延迟场景。我们实测过 1000 台设备每秒上报 1 条数据(约 1KB/条)时的中间件表现:
| 中间件 | 10k msg/s 持续压测 | 消费端延迟 P99 | 运维复杂度 | SpringBoot 集成难度 |
|---|---|---|---|---|
| RabbitMQ | ✅ 达标(需 3 节点镜像队列) | 120ms | 高(需调优 Erlang VM 内存) | 中(spring-boot-starter-amqp) |
| Apache Kafka | ✅ 超额(单节点 30k+) | 18ms | 中(ZooKeeper 已弃用,KRaft 模式简化) | 低(spring-kafka+@KafkaListener) |
| Redis Streams | ⚠️ 边界模糊(超 50k QPS 易阻塞) | 45ms | 低 | 低 |
所以本工程选用 Kafka。配置要点:
spring: kafka: bootstrap-servers: kafka-server:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer properties: acks: all # 关键!确保不丢数据 retries: 3 consumer: group-id: iot-data-consumer-group auto-offset-reset: latest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer properties: spring.json.trusted.packages: "*" # 允许反序列化任意 POJO发送逻辑极简:
@Autowired private KafkaTemplate<String, DeviceStandardData> kafkaTemplate; public void pushToKafka(DeviceStandardData data) { kafkaTemplate.send("iot-raw-data", data.getDeviceId(), data); }注意:
acks: all意味着 Leader 和所有 ISR(In-Sync Replica)都写成功才返回 ACK。这是物联网场景下数据可靠性底线,宁可吞吐降 20%,也不能丢设备上报。
4. 设备管理与安全加固:从“能连上”到“连得稳、管得住”
4.1 设备注册与鉴权:JWT + 设备证书双向认证双保险
HTTP/WebSocket/TCP 三种协议都要鉴权,但方式不同:
- HTTP:设备携带
Authorization: Bearer <JWT>,Token 由设备首次注册时颁发,有效期 30 天,含deviceId和exp; - WebSocket:握手阶段在
Sec-WebSocket-ProtocolHeader 传 Token,服务端HandshakeInterceptor提前校验; - TCP:设备连接后,首帧必须是
AUTH {cert_hash},服务端查t_device.certificate_hash匹配。
JWT 生成示例(使用jjwt-api):
public String generateDeviceToken(String deviceId) { return Jwts.builder() .subject(deviceId) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000)) // 30天 .signWith(SignatureAlgorithm.HS256, "iot-secret-key-2024") // 生产环境请从 Vault 加载 .compact(); }提示:
HS256密钥必须环境隔离。开发用application-dev.yml写死,生产必须通过spring.cloud.config.server或 HashiCorp Vault 注入,禁止硬编码在代码里。曾有项目因密钥泄露,导致攻击者伪造设备 Token 向平台注入虚假温度数据。
4.2 心跳与离线检测:用 ScheduledTask + Redis 实现亚秒级感知
设备心跳不能只靠 TCP KeepAlive(Linux 默认 2 小时才探测)。我们用应用层心跳 + Redis 分布式锁实现 5 秒级离线判定:
@Component public class DeviceHeartbeatMonitor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Scheduled(fixedDelay = 5000) // 每5秒扫描一次 public void checkOfflineDevices() { // 1. 获取所有设备最后心跳时间(Redis 中以 deviceId 为 key,value 为 timestamp) Set<String> allDeviceKeys = redisTemplate.keys("device:last_heartbeat:*"); if (CollectionUtils.isEmpty(allDeviceKeys)) return; // 2. 批量获取并判断超时(Redis pipeline 减少网络往返) List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (String key : allDeviceKeys) { connection.get(key.getBytes()); } return null; }); // 3. 更新 DB 状态(注意:DB 更新必须加分布式锁,避免多实例重复操作) for (int i = 0; i < allDeviceKeys.size(); i++) { String deviceId = allDeviceKeys.stream() .filter(k -> k.contains(":")) .map(k -> k.split(":")[2]) .findFirst() .orElse(""); Long lastTs = (Long) results.get(i); if (System.currentTimeMillis() - lastTs > 15000) { // 超过15秒未心跳 String lockKey = "lock:device:offline:" + deviceId; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { deviceRepository.updateOnlineStatus(deviceId, false); redisTemplate.delete(lockKey); } } } } }4.3 防重放与限流:Guava RateLimiter + 时间戳窗口双校验
设备可能因网络抖动重发同一条数据,必须去重。方案是设备 ID + 时间戳哈希 + Redis Set 缓存 5 分钟:
public boolean isDuplicate(String deviceId, long timestamp) { String cacheKey = "dup:" + deviceId; String hash = DigestUtils.md5Hex(deviceId + ":" + timestamp); Boolean added = redisTemplate.opsForSet() .add(cacheKey, hash); redisTemplate.expire(cacheKey, Duration.ofMinutes(5)); return !Boolean.TRUE.equals(added); } // 在 DeviceDataReceiver.receive() 开头调用 if (isDuplicate(packet.getDeviceId(), packet.getTimestamp())) { log.debug("Duplicate packet ignored: {}@{}", packet.getDeviceId(), packet.getTimestamp()); return; }同时对单设备 IP 做速率限制(防恶意刷接口):
@Component public class DeviceRateLimiter { private final Cache<String, RateLimiter> rateLimiters = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10000) .build(key -> RateLimiter.create(5.0)); // 每秒最多5次 public boolean tryAcquire(String ip) { return rateLimiters.get(ip, RateLimiter::create).tryAcquire(); } }注意:
Caffeine是本地缓存,适合单机部署。若集群多实例,必须换RedisRateLimiter(基于 Lua 脚本原子操作),否则限流失效。
5. 避坑指南:这 4 个错误让 70% 的物联网 SpringBoot 项目上线即崩
5.1 现象:设备连接数超过 1000 后,CPU 暴涨到 95%,netstat -an | grep :8081显示大量TIME_WAIT
原因:TCP Server 默认关闭SO_REUSEADDR,且未设置连接复用。每个断开连接在内核中停留 60 秒(2MSL),导致端口耗尽。
解决:在TcpDeviceServer初始化时显式开启复用:
bootstrap.option(ChannelOption.SO_REUSEADDR, true) // 关键! .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true); // 禁用 Nagle 算法,降低小包延迟同时 Linux 内核调优(/etc/sysctl.conf):
net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.core.somaxconn = 655355.2 现象:设备上报 JSON 中中文字段乱码,日志显示{"temp":"???"}
原因:SpringBoot 3.x 默认字符集为 UTF-8,但某些老旧设备(如部分国产 DTU)发送 HTTP POST 时未声明Content-Type: application/json; charset=utf-8,Tomcat 8.5+ 会 fallback 到 ISO-8859-1。
解决:强制全局 UTF-8 解码:
@Configuration public class WebConfig { @Bean public HttpMessageConverter<String> stringHttpMessageConverter() { StringHttpMessageConverter converter = new StringHttpMessageConverter(StandardCharsets.UTF_8); converter.setWriteAcceptCharset(false); return converter; } }并在application.yml中追加:
server: servlet: context-path: / tomcat: uri-encoding: UTF-8 spring: http: encoding: charset: UTF-8 force: true5.3 现象:Kafka 消费端持续报Offset commit failed,数据重复消费
原因:@KafkaListener方法内做了耗时操作(如同步调用外部 HTTP API),导致 Consumer Group 心跳超时被踢出,Rebalance 后重新分配分区,旧 offset 未提交。
解决:消费逻辑必须异步化 + 手动提交 offset:
@KafkaListener(topics = "iot-raw-data", groupId = "iot-consumer-group") public void listen(ConsumerRecord<String, DeviceStandardData> record, Acknowledgment ack) { try { // 1. 业务处理(必须快!) processDeviceData(record.value()); // 2. 手动提交 offset ack.acknowledge(); } catch (Exception e) { log.error("Failed to process record", e); // 3. 失败时不 ack,让 Kafka 重试(需配置 max.poll.interval.ms > 处理耗时) } }并在application.yml中调大心跳间隔:
spring: kafka: consumer: properties: max.poll.interval.ms: 300000 # 5分钟,足够处理慢逻辑5.4 现象:actuator/health返回 DOWN,但服务明明在运行
原因:默认 Health Indicator 包含DataSourceHealthIndicator,而 H2 数据库在应用启动后未创建任何表(ddl-auto: none),导致健康检查失败。
解决:禁用非必要 Indicator,或重写 DataSource 检查逻辑:
@Component public class CustomDataSourceHealthIndicator extends AbstractHealthIndicator { private final DataSource dataSource; public CustomDataSourceHealthIndicator(DataSource dataSource) { this.dataSource = dataSource; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { try (Connection connection = dataSource.getConnection()) { connection.createStatement().execute("SELECT 1"); // 简单探活 builder.status(Status.UP).withDetail("database", "PostgreSQL").build(); } catch (SQLException e) { builder.status(Status.DOWN).withDetail("error", e.getMessage()).build(); } } }并在application.yml中排除默认:
management: endpoint: health: show-details: when_authorized endpoints: web: exposure: include: health,info,metrics,threaddump health: db: show-details: never6. 生产就绪 checklist:从源码到交付,我每天上线前必做的 7 件事
你拿到的这份源码,不是“写完就能跑”,而是“按 checklist 走完才能上线”。以下是我带团队交付 12 个物联网项目沉淀下来的硬性动作,少一步,线上就可能出事。
6.1 日志分级与归档:用 Logback 实现设备级追踪
物联网问题定位,90% 依赖日志。必须做到:按设备 ID 切分日志文件 + ERROR 级别自动告警 + TRACE 级别可开关。
logback-spring.xml关键配置:
<appender name="DEVICE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/device/${DEVICE_ID:-unknown}.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/device/${DEVICE_ID:-unknown}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 动态 MDC 设备 ID --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] [%X{deviceId}] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>在DeviceDataReceiver.receive()开头注入 MDC:
MDC.put("deviceId", packet.getDeviceId()); try { // ... 处理逻辑 } finally { MDC.clear(); }这样查问题时,直接
grep "DEVICE-ABC123" logs/device/DEVICE-ABC123.log就能拿到该设备全生命周期日志,不用在 10G 通用日志里翻。
6.2 JVM 参数调优:针对物联网长连接场景的 GC 策略
默认-Xmx对物联网服务是灾难。我们用 G1 GC + 固定堆内存:
java -server \ -Xms2g -Xmx2g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+UseStringDeduplication \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/opt/iot-collector/dumps/ \ -jar iot-collector-server.jar-Xms2g -Xmx2g:避免运行时堆伸缩,减少 GC 波动;MaxGCPauseMillis=200:G1 目标停顿时间,匹配设备心跳敏感度;UseStringDeduplication:设备上报 JSON 中字段名(如"temperature")高度重复,此参数可节省 15% 堆内存。
6.3 Docker 镜像瘦身:从 850MB 到 180MB 的三层精简
原始openjdk:17-jdk-slim镜像含大量调试工具,物联网服务完全不需要。我们用JDK 17 JRE + Spring Boot Layertools + 多阶段构建:
# 构建阶段 FROM maven:3.9.4-openjdk-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:17-jre-slim RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/* ENV TZ=Asia/Shanghai WORKDIR /app # 使用 Spring Boot 3.2+ 的 layertools 提取 runtime layer RUN java -Djarmode=layertools -jar /app/target/iot-collector-server.jar extract # 只拷贝必要 layer COPY --from=build /app/target/iot-collector-server.jar/dependencies/ ./dependencies/ COPY --from=build /app/target/iot-collector-server.jar/spring-boot-loader/ ./spring-boot-loader/ COPY --from=build /app/target/iot-collector-server.jar/classes/ ./classes/ COPY --from=build /app/target/iot-collector-server.jar/libs/ ./libs/ ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]6.4 健康检查端点增强:不只是 UP/DOWN,还要看设备在线率
/actuator/health默认只检查 DB、Disk、Redis 连通性。物联网服务必须加设备健康指标:
@Component public class DeviceHealthIndicator implements HealthIndicator { @Autowired private DeviceRepository deviceRepository; @Override public Health health() { long total = deviceRepository.count(); long online = deviceRepository.countByOnlineStatusTrue(); double onlineRate = total == 0 ? 0.0 : (double) online / total * 100; Health.Builder builder = Health.up(); if (onlineRate < 95.0) { builder = Health.down(); } return builder .withDetail("totalDevices", total) .withDetail("onlineDevices", online) .withDetail("onlineRatePercent", onlineRate) .withDetail("offlineDevices", total - online) .build(); } }这样 Prometheus 抓取health指标时,就能画出「设备在线率趋势图」,比单纯看服务进程是否存活有价值得多。
6.5 配置外置化:用 Config Server 管理 200+ 台设备的差异化参数
当设备规模超 100 台,不可能每台设备写一个application-device-xxx.yml。我们用 Spring Cloud Config Server + Git Backend:
- Git 仓库结构:
/config-repo/ ├── application.yml # 全局默认 ├── iot-collector-server/ │ ├── dev.yml # 开发环境 │ └── prod.yml # 生产环境(含 Kafka 地址、DB 密码) └── devices/ ├── device-abc123.yml # 设备 ABC123 的专属配置:采样间隔、告警阈值 └── device-def456.yml # 设备 DEF456 的专属配置
客户端配置:
spring: config: import: optional:configserver:http://config-server:8888 cloud: config: discovery: enabled: true service-id: config-server fail-fast: true启动时自动加载devices/device-${deviceId}.yml,实现千人千面。
6.6 压测基线:用 JMeter 模拟 5000 设备并发,必须达标的 4 个数字
上线前不做压测,等于裸奔。我们固定执行以下 JMeter 脚本(iot-device-sim.jmx):
| 指标 | 达标线 | 测试方法 |
|---|---|---|
| HTTP 接口 P95 延迟 | ≤ 120ms | 5000 线程,Ramp-up 300 秒,每秒发 1 个 JSON |
| WebSocket 连接成功率 | ≥ 99.99% | 5000 并发连接,保持 1 小时,统计断连数 |
| TCP Server 吞吐 | ≥ 8000 msg/s | Netty Client 模拟 5000 设备,每 2 秒发 1 帧 |
| JVM Full GC 频率 | 0 次/小时 | jstat -gc <pid>持续监控 1 小时 |
不达标?先查 GC 日志,再查 Kafka 消费 lag,最后看 Netty EventLoop 线程是否打满。别急着加机器,90% 的性能问题出在代码层。
6.7 回滚预案:如何 3 分钟内切回上一版,且不丢设备数据
最怕上线后发现新版本设备注册失败。我们的回滚不是git checkout,而是滚动重启 + 数据兼容:
- 新版本启动时,用
spring.profiles.active=v2,老版本保持v1; - Nginx 配置灰度路由:
upstream iot-backend { server 10.0.1.10:8080 weight=95; # v1 server 10.0.1.11:8080 weight=5; # v2 } - 若 v2 异常,立刻
weight=0切走流量; - 关键:v2 版本必须兼容 v1 的数据库 Schema 和 Kafka Topic Schema,新增字段加
@Column(nullable = true),绝不删字段。
我带过的项目里,最深的教训是:永远假设你的代码会出错,但数据库和消息队列不会。所以所有升级,第一原则是「向前兼容」,第二才是「功能增强」。
希望帮到你。
本文还有配套的精品资源,点击获取