 SseEmitter vs Flux 的本质区别与底层原理解析
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/10/15, 09:17:23
