SseEmitter vs Flux 的本质区别与底层原理解析
# SseEmitter vs Flux 的本质区别与底层原理解析
在实际项目中,我们经常需要实现实时向客户端推送数据的流式接口,例如:
- 实时日志推送
- 消息订阅
- 数据监控大屏
- Web 聊天系统
在 Java 的 Spring Boot 框架中,主流方案有两个:
- 使用 Spring MVC 提供的
SseEmitter
实现 SSE(Server-Sent Events) - 使用 Spring WebFlux 的
Flux
实现响应式数据流
它们都能实现服务端推数据给客户端,但底层机制和性能模型完全不同。本文将不仅介绍用法,还会深入探讨为什么 SseEmitter
会“阻塞线程”、而 Flux
不会,从而解答你在高并发或性能场景下的关键技术疑问。
# 方案一:使用 SseEmitter
实现 SSE 接口
@RestController
public class SseStreamController {
@GetMapping("/stream/sse")
public SseEmitter streamSse() {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 超时时间 30 分钟
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 1; i <= 10; i++) {
emitter.send("当前 event 数据:" + i);
Thread.sleep(1000);
}
emitter.complete(); // 发送完毕
} catch (Exception e) {
emitter.completeWithError(e);
} finally {
executor.shutdown();
}
});
return emitter;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
使用 SseEmitter
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1
2
3
4
2
3
4
# 为什么 SseEmitter.send()
是阻塞的?
这其实是很多开发者的疑惑:我们不是用了 IO 多路复用的 Web 服务器(如 Tomcat)吗?为什么还会阻塞线程?
答案是:虽然底层服务器支持 IO 多路复用,但你用的 API 是阻塞的。
具体来说:
SseEmitter.send()
最终调用的是HttpServletResponse.getOutputStream().write(...)
- 这是一个阻塞式系统调用,底层会转为
write(fd, buf, len)
,这里没有用IO多路复用阻塞了线程 - 如果 socket 写缓冲区满,线程就会被操作系统挂起,直到 socket 可写
所以本质上是线程“在等 socket 可写”,而不是 socket 本身卡了。
换句话说:不是 IO 多路复用失效了,而是你没有使用它。
# 多线程开销问题
每一个 SseEmitter 连接都会占用一个线程,如果有 500 个并发 SSE 连接,那么:
- 默认 Tomcat 最大线程池(
maxThreads=200
)会被迅速耗尽; - 超过的请求将排队或被拒绝;
- 即便主线程快速返回,实际的处理线程依旧卡在
send()
上
# 方案二:使用 Flux
+ WebFlux 实现响应式流
@RestController
public class ReactiveStreamController {
@GetMapping(value = "/stream/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamFlux() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "响应式流数据:" + (i + 1))
.take(10);
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
使用 WebFlux
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
1
2
3
4
2
3
4
这段代码会每秒向客户端推送一条 SSE 数据,持续 10 次自动结束。
# 为什么 Flux
不会阻塞线程?
Flux
背后的运行机制基于 Reactor + Netty + 事件驱动模型,采用:
- IO 多路复用(epoll/kqueue)
- 非阻塞 socket
- 状态机控制写入操作
核心逻辑:
- 不调用阻塞式
write()
系统调用; - 而是监听 socket 可写事件(事件驱动)
- 写缓冲区满时挂起该操作,不会阻塞线程
- 下一次可写时继续发送
因此:
哪怕有 1 万个连接,仍然可以只用几十个线程处理所有网络 IO
这是 WebFlux 最大的优势,也是它更适合高并发实时推送场景的关键。
# 编程模型对比:SseEmitter vs Flux
对比维度 | SseEmitter(Servlet) | Flux(WebFlux) |
---|---|---|
编程模型 | 每请求一个线程 | Reactor 响应式模型 |
是否阻塞 | 是,send() 阻塞写 | 否,基于事件驱动 |
依赖线程数 | 每个连接都占用线程 | 少量线程处理所有连接 |
并发能力 | 线程池受限,瓶颈明显 | 高并发强,天然支持背压 |
适用场景 | 简单推送、小规模使用 | 高频更新、海量连接、大屏等 |
若你尝试用 JMeter
模拟 500 个并发请求:
SseEmitter
版本很快就会因为线程池耗尽报错(503);Flux
版本依然稳定,CPU 负载可控,延迟低
因此,建议如下:
- ❗ SseEmitter:适合低频、少量连接的 SSE 场景(如管理员后台)
- ✅ WebFlux + Flux:推荐用于所有需要高并发、持续推送的核心系统
# 总结
在 Spring Boot 中实现服务端实时推送接口时:
- SseEmitter 提供了简便的方式实现 SSE,但本质依赖阻塞式 IO,每个连接占用一个线程,限制并发能力;
- Flux + WebFlux 则通过响应式编程与非阻塞 IO 实现了真正的高并发推送能力,是现代架构中更优选。
理解这两者背后的编程模型与 IO 原理,对于设计高性能服务端系统至关重要。
编辑 (opens new window)
上次更新: 2025/05/12, 03:06:46
← 正则表达式