
很多程序员第一次开发 MES(Manufacturing Execution System,制造执行系统)时,都会遇到这样一个需求:
如何让设备状态实时刷新?
看起来,这只是一个很小的功能。
但真正做过 MES 的人都知道,它背后涉及的不仅仅是前端刷新,而是整个系统的数据推送架构。
今天,我们就从一个真实的 MES 场景出发,聊聊为什么越来越多的工业系统开始采用 SSE(Server-Sent Events),以及如何在 Spring Boot 中实现一个高可靠的流式推送架构。
一、MES中的一个真实场景

假设现在有一个离散制造车间。
车间内有 30 台设备正在加工。
MES 首页的大屏需要实时展示:
text
设备A:运行中
设备B:待料
设备C:报警
设备D:加工完成
与此同时,还需要实时刷新:
- 当前工单执行进度
- 实时产量
- 设备利用率(OEE)
- 告警信息
- 操作员状态
如果采用传统 HTTP 请求,最容易想到的方案就是:
text
浏览器
│
每5秒请求一次
│
Spring Boot
│
查询数据库
│
返回最新状态
刚开始看起来没有问题。
但是随着项目规模扩大,很快问题就出现了。
二、轮询为什么越来越慢?

假设:
一个车间有:
- 100 个在线客户端
- 每个客户端打开 3 个实时页面
- 每 2 秒刷新一次
那么服务器每分钟需要处理:
text
100 × 3 × 30 = 9000 次请求
更关键的是:
这 9000 次请求中,大部分返回的数据都是:
text
没有变化。
也就是说:
浏览器不停地问服务器:
"设备状态变了吗?"
服务器一次次回答:
"没有。"
这种通信方式显然非常低效。
而真正发生变化的,也许只是:
text
09:21:15
设备A:
RUNNING
↓
FAULT
整个系统真正需要推送的数据,其实只有这一条。
三、真正需要的是"服务器主动通知"

MES 更希望采用这样的通信方式:
text
设备状态变化
│
MES收到事件
│
立即通知浏览器
│
浏览器更新页面
也就是说:
不是浏览器不停询问。
而是:
服务器有变化再通知客户端。
这就是 SSE 的核心思想。
四、什么是SSE?

SSE(Server-Sent Events)可以理解为:
服务器主动向浏览器持续发送数据。
整个通信过程只有一次 HTTP 请求。
连接建立后:
浏览器一直保持连接。
服务器只要有新的数据:
立即写入连接。
例如:
text
Browser
│
──────────────►
Spring Boot
连接保持不断开
Spring Boot
write()
flush()
write()
flush()
Browser
不断收到数据
整个过程中:
浏览器无需再次发起请求。
五、为什么MES特别适合SSE?

MES 有一个非常典型的特点:
数据变化频繁,但客户端几乎不需要向服务器发送实时消息。
例如:
设备状态:
text
IDLE
↓
RUNNING
↓
FAULT
↓
RUNNING
工单状态:
text
CREATED
↓
RELEASED
↓
RUNNING
↓
COMPLETED
生产数量:
text
356
↓
357
↓
358
↓
359
这些数据:
全部都是:
服务器主动产生。
浏览器只是:
接收。
因此:
SSE 与 MES 的业务模型天然匹配。
六、为什么不是WebSocket?

很多人第一反应:
实时通信就应该使用 WebSocket。
其实未必。
对于 MES 来说:
浏览器主要承担:
text
数据显示
真正的数据流:
几乎都是:
text
设备
↓
MES
↓
浏览器
浏览器极少需要:
持续向服务器发送消息。
因此:
SSE 已经完全能够满足需求。
相比 WebSocket:
SSE:
- 基于 HTTP
- 浏览器原生支持
- 自动重连
- 部署简单
- 更容易经过 Nginx、网关和负载均衡
对于:
- 实时看板
- 工单监控
- OEE监控
- 告警推送
都是非常合适的选择。
七、Spring Boot如何实现SSE?

Spring MVC 已经提供了现成的支持:
java
@GetMapping("/device/stream")
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter(0L);
emitterMap.put(userId, emitter);
return emitter;
}
其中:
java
new SseEmitter(0L)
表示:
text
永不超时
随后:
当设备状态变化时:
java
emitter.send(
SseEmitter.event()
.name("device")
.data(deviceStatus)
);
浏览器立即收到:
javascript
eventSource.onmessage = function(event){
console.log(event.data);
}
整个过程无需再次建立连接。
八、真正的实时推送不是"查询数据库"

各位一定要避免一个误区,就是不停地查询数据库来实现"实时"的效果,这样对资源是极大的浪费
很多项目都会这样写:
text
设备变化
↓
更新数据库
↓
浏览器轮询数据库
实际上:
MES 完全没有必要这样设计。
更合理的方式应该是:
text
PLC
↓
设备采集服务
↓
发布设备事件
↓
MES状态机
↓
SSE推送
↓
浏览器
设备事件一旦产生:
立即推送。
数据库只是:用于持久化。
而不是:用于实时通信。
九、生产环境需要解决哪些问题?

真正上线后,仅仅使用 SseEmitter 还远远不够。
通常还需要考虑以下几个方面。
1. 连接管理
每一个浏览器都会对应一个 SseEmitter。
需要统一维护连接池。
例如:
text
ConcurrentHashMap<UserId, SseEmitter>
避免连接丢失。
2. 心跳机制
很多网关会自动断开长连接。
因此:
服务器需要定时发送:
text
heartbeat
保持连接存活。
3. 断线清理
浏览器关闭后:
必须及时:
java
removeEmitter()
否则:
连接越来越多。
最终导致:
内存泄漏。
4. 自动重连
浏览器网络异常:
SSE 可以自动重新建立连接。
这也是它相比普通 HTTP 更大的优势。
5. 异步推送
设备状态变化:
绝不能阻塞 PLC 数据采集线程。
通常采用:
text
设备事件
↓
消息队列
↓
推送线程池
↓
SSE
这样即使:
1000 个客户端在线。
设备采集仍然不会受到影响。
十、MES实时推送的推荐架构

实际项目中,我们更推荐采用事件驱动架构。
text
PLC
│
▼
设备采集服务
│
▼
设备事件(Event)
│
▼
MES业务状态机
│
▼
事件总线
│
▼
SSE推送服务
│
▼
浏览器
这里:
SSE 只是:
最后一公里。
真正核心的是:
整个系统采用:
事件驱动。
设备状态变化。
工单状态变化。
告警产生。
生产完成。
都应该抽象成:
统一事件。
这样:
不仅可以推送浏览器。
未来:
消息队列、AI分析、数据湖、BI平台,都可以复用同一套事件流。
十一、结语
很多程序员学习 SSE,关注的是:
Spring Boot 如何使用
SseEmitter?
但对于工业软件来说:
真正的问题并不是:
如何推送数据。
而是:
什么时候推?
推什么?
由谁触发推送?
只有把设备事件、MES 状态机和实时推送结合起来,
SSE 才真正成为制造执行系统中的一部分,而不是一个孤立的通信技术。
对于 MES 来说,实时推送的价值,从来不是让页面"动起来"。
而是让设备状态、工单执行和生产过程,以最低延迟、最高可靠性的方式,第一时间传递给真正需要它的人。