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),所有代码和踩坑记录都会发出来。欢迎关注,一起卷物联网后端。

相关推荐
MuMuMu12233 分钟前
越华环保集团碳惠小屋:物联网 + 区块链赋能碳普惠终端,构建可信个人碳账户体系
物联网
注塑机数据采集联网19 分钟前
如何解决注塑机数据采集联网对接MES系统数据WiFi传输数据不稳问题
大数据·数据库·物联网·数据挖掘·数据分析
liubinkaixin2 小时前
海上风电叶片试验的上千片应变片,长线干扰怎么解
物联网
数字新视界2 小时前
机房动环监控提升数据中心安全性
物联网·3d·数字孪生·三维可视化·数据可视化
serene942 小时前
Modbus“出道史”:1979年诞生,后来一路火到现在
物联网·开发·modbus·通讯协议·模拟器
黄鹂鸟科技3 小时前
软件定制开发设计要点:从需求梳理、原型确认到敏捷交付与持续维护
后端·物联网·系统架构
虎王物联4 小时前
随身WiFi什么时候能恢复出厂:去控机和硬刷机的差别
物联网·随身wifi
IT规划师4 小时前
MQTT实战 - QoS 0-1-2 到底怎么选?一张图看懂(四)
物联网·mqtt·.net core·iot·qos
钱工的门窗技术笔记17 小时前
智能门窗的边界与前提:为什么“智能”不能替代本体性能
网络·物联网·智能家居