Yang's blog Yang's blog
首页
后端开发
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xiaoyang

尽人事,听天命
首页
后端开发
密码学
机器学习
命令手册
关于
友链
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • SpringCloud

    • 微服务架构介绍
    • SpringCloud介绍
    • Spring Cloud:生产者与消费者
    • Spring Cloud Eureka:构建可靠的服务注册与发现
    • Spring Cloud Ribbon:负载均衡
    • Spring Cloud Fegin:服务调用
    • Spring Cloud Hystrix:熔断器
    • Spring Cloud Zuul:统一网关路由
    • Spring Cloud Config:配置中心
  • Java后端框架

    • LangChain4j

      • 介绍
      • 快速开始
      • Chat and Language Models
      • Chat Memory
      • Model Parameters
      • Response Streaming
      • AI Services
      • Agent
      • Tools (Function Calling)
      • RAG
      • Structured Outputs
      • Classification
      • Embedding (Vector) Stores
      • Image Models
      • Quarkus Integration
      • Spring Boot Integration
      • Kotlin Support
      • Logging
      • Observability
      • Testing and Evaluation
      • Model Context Protocol
  • 八股文

    • 操作系统
    • JVM介绍
    • Java多线程
    • Java集合框架
    • Java反射
    • JavaIO
    • Mybatis介绍
    • Spring介绍
    • SpringBoot介绍
    • Mysql
    • Redis
    • 数据结构
    • 云计算
    • 设计模式
    • 计算机网络
    • 锁核心类AQS
    • Nginx
  • 前端技术

    • 初识Vue3
    • Vue3数据双向绑定
    • Vue3生命周期
    • Vue-Router 组件
    • Pinia 集中式状态存储
  • 中间件

    • RocketMQ
  • 开发知识

    • 请求参数注解
    • 时间复杂度和空间复杂度
    • JSON序列化与反序列化
    • Timestamp vs Datetime
    • Java开发中必备能力单元测试
    • 正向代理和反向代理
    • 什么是VPN
    • 后端服务端主动推送消息的常见方式
    • 正则表达式
    • SseEmitter vs Flux 的本质区别与底层原理解析
      • 方案一:使用 SseEmitter 实现 SSE 接口
        • 为什么 SseEmitter.send() 是阻塞的?
        • 多线程开销问题
      • 方案二:使用 Flux + WebFlux 实现响应式流
        • 为什么 Flux 不会阻塞线程?
      • 编程模型对比:SseEmitter vs Flux
      • 总结
  • 后端开发
  • 开发知识
xiaoyang
2025-05-12
目录

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

使用 SseEmitter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
1
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

使用 WebFlux:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
1
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

← 正则表达式

最近更新
01
操作系统
03-18
02
Nginx
03-17
03
后端服务端主动推送消息的常见方式
03-11
更多文章>
Theme by Vdoing | Copyright © 2023-2025 xiaoyang | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式