Skip to content
xiaoyang 的头像xiaoyang发布于 2025-05-122,767 字阅读约 9 分钟浏览加载中

SseEmitter vs Flux 的本质区别与底层原理解析 ​

在实际项目中,我们经常需要实现实时向客户端推送数据的流式接口,例如:

  • 实时日志推送
  • 消息订阅
  • 数据监控大屏
  • Web 聊天系统

在 Java 的 Spring Boot 框架中,主流方案有两个:

  • 使用 Spring MVC 提供的 SseEmitter 实现 SSE(Server-Sent Events)
  • 使用 Spring WebFlux 的 Flux 实现响应式数据流

它们都能实现服务端推数据给客户端,但底层机制和性能模型完全不同。本文将不仅介绍用法,还会深入探讨为什么 SseEmitter 会“阻塞线程”、而 Flux 不会,从而解答你在高并发或性能场景下的关键技术疑问。


方案一:使用 SseEmitter 实现 SSE 接口 ​

java
@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;
    }
}

使用 SseEmitter:

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

为什么 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 实现响应式流 ​

java
@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);
    }
}

使用 WebFlux:

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

这段代码会每秒向客户端推送一条 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 原理,对于设计高性能服务端系统至关重要。

DISCUSSION

评论区

欢迎分享你的思考、补充或问题。

基于 VitePress 搭建