Spring Boot 实时数据推送怎么做?把 3 秒轮询升级成 WebSocket(附断线降级方案)

设备看板要"实时",新手第一反应是前端 setInterval 轮询。能用,但有两个天生的毛病。这篇用我设备接入平台里的真实代码,讲清楚轮询 → WebSocket 的完整升级路径,以及最容易被忽略的降级方案。

先看最终效果------这是我平台的真实运行截图,温湿度数据 2 秒一刷:

设备在线状态、最新温湿度、60 点实时曲线、最新数据帧,全部来自 MQTT 真实上报链路,不是静态假数据。

轮询的两个毛病

我第一版看板就是前端每 3 秒 GET /api/devices/overview 一次。能用,但:

  1. 浪费:数据没变化也在发请求,100 个打开的页面 = 每分钟 2000 次无效请求
  2. 实时性上限:数据变化后最多要等 3 秒才能看到,这个延迟写死在轮询间隔里

所以升级成 WebSocket:服务端控制节奏,一条连接服务所有刷新需求。

核心实现:一个 Handler 干三件事

java 复制代码
@Component
@RequiredArgsConstructor
public class OverviewWebSocketHandler extends TextWebSocketHandler {

    private final OverviewService overviewService;
    private final ObjectMapper objectMapper;

    private final Set<WebSocketSession> sessions = new CopyOnWriteArraySet<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        sendSnapshot(session); // 连上立刻给一帧,前端不用等下一个推送周期
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
    }

    /** 每 2 秒向所有在线连接广播一次概览快照 */
    @Scheduled(fixedDelayString = "${iot.ws.push-interval-ms:2000}", initialDelay = 5000)
    public void broadcast() {
        if (sessions.isEmpty()) {
            return;
        }
        String payload = snapshot();
        for (WebSocketSession session : sessions) {
            try {
                if (session.isOpen()) {
                    synchronized (session) {
                        session.sendMessage(new TextMessage(payload));
                    }
                } else {
                    sessions.remove(session);
                }
            } catch (IOException e) {
                log.warn("推送失败,剔除连接 {}: {}", session.getId(), e.getMessage());
                sessions.remove(session);
            }
        }
    }
}

三个实现取舍,每一个都是面试考点:

① 固定 2 秒推全量快照,而不是"有变化才推"

变化检测需要比对逻辑(脏标记/增量计算),而概览快照本身很小。先简单正确,将来设备多了再上增量------这个取舍比"上来就搞增量"聪明。

② sessions 用 CopyOnWriteArraySet

连接数少、每次广播都要遍历全部连接:读多写少的场景,写时复制比加锁更合适。如果连接数上万,这个选择就要重新评估。

③ 推送失败直接踢掉坏连接

sendMessage 抛 IOException 说明连接已坏(半开连接),不做复杂的健康检查,直接从集合剔除,等前端自己重连。简单,且不会泄漏死连接。

两个容易漏的细节

连上立刻推一帧 :afterConnectionEstablished 里马上 sendSnapshot。不然前端要干等最多 2 秒白屏。

sendMessage 要加锁 :synchronized (session)。Spring 的原生 WebSocketSession 不支持并发写 ,定时广播和连接建立时的首帧推送可能同时写同一个 session,不加锁会偶发 IllegalStateException: TEXT_PARTIAL_WRITING------这个报错搜不到几篇中文资料,我替你踩过了。

降级方案:WebSocket 挂了页面也不能死

网络环境复杂,公司代理、校园网都可能掐 WebSocket 长连接。所以轮询没有删掉,而是做成降级路线:

markdown 复制代码
前端逻辑:
1. 优先建 WebSocket,收到推送就刷新
2. onclose / 超时未收到消息 → 自动切回 3 秒轮询
3. 每隔一段时间尝试重建 WebSocket,成功就切回推送

后端代码一行不用改------因为推送的数据结构和 GET /api/devices/overview 完全相同 (同一个 Service 构建、同一个 DTO),前端一套渲染逻辑两处复用。这是这次升级里我觉得最值的设计决策:数据契约不变,传输层随便换。

升级后的刷新效果(曲线自动延伸、右上角"已刷新"时间戳每 2 秒跳动):

面试怎么讲

被问"实时推送怎么做的",三层答法:

  1. 选型:看板类低频全量刷新,服务端定时推快照就够;如果是消息类要毫秒级,才考虑按事件推
  2. 并发:CopyOnWriteArraySet 适配读多写少;session 写要加锁,Spring 原生 session 不支持并发写
  3. 可靠性 :坏连接直接剔除等重连;必须保留轮询降级,WebSocket 不是可用性承诺而是体验增强

"必须保留降级"这一点说出来,面试官会知道你考虑过生产环境------WS 在复杂网络里就是会断,功能永远可用的兜底思维比技术本身值钱。

下一步

现在看板的数据链路是:模拟设备 → EMQX → 消费入库 → 定时聚合 → WebSocket 广播。再往下一步是把"设备注册/密钥管理"补成完整的管理端,然后上 Kafka 把消费和接入解耦------那是下一篇文章的事。


我是软件工程在读,正在从零搭一个物联网设备接入平台(Spring Boot + EMQX + MySQL),所有代码和踩坑记录都会发出来。欢迎关注,一起卷物联网后端。

相关推荐
今年下半年7 小时前
记录一次IBMS物联网项目的架构设计及技术栈应用
物联网·websocket·网络协议·tcp/ip·微服务·udp·kafka
水墨长天15 小时前
物联网协议 MQTT 快速开发与实践
后端·物联网
EatFans15 小时前
全栈自造Status Deck(三):桌面端上位机程序开发
物联网
调试优选官6 天前
IoT物联网系统定制落地实践:从设备接入、协议网关、平台分层到业务应用,如何做技术选型、交付验收、责任边界与长期运维成本评估及迁移安排
物联网·iot·成本分析
华允物联-HUAIOT6 天前
工业路由器和DTU在联网方式上的区别
物联网
wuyk5556 天前
《WiFi 嵌入式物联网开发全套实战》| 第 16 章 ESP32 AP+STA 双模共存原理与工程坑点
网络·stm32·物联网
码流子6 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
绿蕉6 天前
配电房的“AI听诊师”:可视化声纹局放智能巡检系统与通讯保障
物联网
chshang19926 天前
工业路由器是什么?浅谈5G工业网络中的IR602
网络·物联网·5g·智能路由器