本节摘要:JVM 服务端以 Spring WebFlux(Reactor + Netty)与 Vert.x(事件总线 + 多语言)为代表;Akka Streams 适合 Actor 与流混合拓扑。选型看团队栈、背压需求与运维可观测性。
ReadStream/WriteStream 与 RS 的对齐方式基于 Reactor + Netty,请求线程不阻塞等待 I/O。Controller 返回 Mono<T> / Flux<T> 时,框架把异步结果写回响应。
@RestController public class UserController { @GetMapping("/users/{id}") public Mono<User> get(@PathVariable String id) { return userRepository.findById(id); // R2DBC Mono } }
SOURCE:Netflix Zuul 2 迁到 Reactor 后单节点吞吐约 +300%,GC 停顿约 -90%——来自线程不再与请求 1:1 绑定,而非单纯「代码变快」。
| 维度 | Spring MVC | WebFlux |
|---|---|---|
| 默认服务器 | Tomcat(阻塞) | Netty(非阻塞) |
| 典型返回 | 对象 / DeferredResult |
Mono / Flux |
| 适用 | CRUD、同步业务 | 高并发 I/O、流式 API |
| 线程模型 | 线程池 + 请求绑定 | 少量 EventLoop + 异步回调 |
WebFlux 与 MVC 最本质的区别在线程模型。MVC 中每个请求占用一个 Tomcat 线程直到响应完成;WebFlux 中请求被 Netty EventLoop 接收,等待期间 EventLoop 去处理其他请求,I/O 完成后再回来继续——一个 EventLoop 同时服务成千上万个请求。这就是 Zuul 2 吞吐 +300% 的来源:不是算法更快,而是同样的硬件不再被「等待」浪费。
@GetMapping("/events") public Flux<ServerSentEvent<String>> events() { return Flux.generate(() -> 0, (i, sink) -> { sink.next(event(i)); return i + 1; }) .limitRate(64) // 每次最多取 64,防上游冲垮 .delayElements(Duration.ofMillis(500)) // 限速输出 .map(e -> ServerSentEvent.builder(e).build()); }
limitRate 在 WebFlux 中承担关键角色:它让下游消费者主动按批请求,配合 delayElements 控制推送节奏。对于 SSE 推送、事件流接口,这两者是防止「服务端把客户端冲垮」的主要手段——服务端要同时考虑下游连接数与消费能力。
Vert.x 以 Event Loop + 垂直扩展著称,HttpServer 与 WebClient 默认非阻塞;4.x 将 pause/resume 与 RS request 映射对齐。
// Vert.x:ReadStream 语义 router.get("/files/:name").handler(ctx -> { ReadStream<Buffer> stream = fileSystem.open( ctx.request().getParam("name"), new OpenOptions()); // 每个 chunk 通过 write 回传,配合 pause/resume 控流 stream.pipe().to(ctx.response().writeQueueFullHandler(v -> { stream.pause(); // 下游写满了就暂停读取 })); });
Vert.x 的 ReadStream.pause()/resume() 是另一条背压实现路径:当下游写队列满时暂停读取上游,空闲时恢复——语义与 RS 的 request 等价但更「流式」。Vert.x 的定位是多语言事件驱动工具包,它自带事件总线(Event Bus),适合微服务间轻量消息与实时应用。
Akka Streams 用 Source/Flow/Sink DSL 隐藏底层 Publisher,适合已有 Akka Actor 系统需要流式 ETL 的团队。
// Akka Streams:图 DSL,可静态验证 val source = Source(1 to 100) val flow = Flow[Int].map(_ * 2).filter(_ > 10) val sink = Sink.foreach(println) source.via(flow).runWith(sink)
Akka Streams 与 Reactor 的思路不同:它把流建模为可静态构造的图(Source/Flow/Sink 组合),运行时由 Actor 承载,天然适合与 Actor 系统混用。背压通过 Actor 的 mailbox 与 request 语义实现。如果团队已有 Akka 基础设施,Akka Streams 比引入 Reactor 更平滑;反之,纯 JVM 团队用 Reactor + WebFlux 更轻。
| 场景 | 推荐 | 理由 |
|---|---|---|
| Spring Boot 存量 + 新网关 | WebFlux | 生态整合、注解熟悉 |
| 多语言/实时双向 | Vert.x | 多语言 + 事件总线 |
| Actor 系统 + 流式 ETL | Akka Streams | 图 DSL、Actor 协同 |
| 简单 CRUD 低并发 | MVC | 响应式收益不足 |
💡 关键直觉:框架再炫,若底层 JDBC 仍
block(),只是「响应式外壳」。
一句话总结:服务端响应式选型没有银弹——WebFlux 靠生态整合取胜,Vert.x 靠多语言与事件总线取胜,Akka Streams 靠图建模与 Actor 协同取胜;共同底线是「运行时非阻塞、每一跳尊重背压」。
| 维度 | Spring WebFlux | Vert.x |
|---|---|---|
| 编程模型 | 注解 + 函数式 Router | 事件处理器 |
| 默认 I/O | Netty | Netty |
| 语言 | Java/Kotlin | Java/Kotlin/JS/Groovy 等 |
| 事件总线 | 无(可配 RSocket) | 内置 Event Bus |
| 生态 | Spring 全家桶 | 自包含 |
| 学习曲线 | 陡(Spring 概念叠加) | 中 |
选择时除性能数字外,还要评估团队认知负荷:WebFlux 把响应式叠加在 Spring 体系之上,团队需同时掌握 Spring 与响应式两套心智;Vert.x 更「纯粹」,但脱离 Spring 生态意味着很多组件要自己选型组合。没有绝对优劣,只有与现有团队栈的契合度。
// 迁移路径一:Spring Boot 渐进式 // 步骤1:新增接口用 WebFlux,旧接口保持 MVC // 步骤2:替换 RestTemplate → WebClient // 步骤3:替换 JDBC → R2DBC(或 boundedElastic 隔离) // 迁移路径二:Vert.x 绿地项目 // 步骤1:引入 Vert.x 核心 + 路由 // 步骤2:接入事件总线做服务间消息 // 步骤3:用 WebClient 替换阻塞 HTTP // 迁移路径三:Akka Streams 存量 Actor // 步骤1:Source/Flow/Sink 接入 Actor // 步骤2:用图 DSL 组装 ETL // 步骤3:接 Backpressure 语义到 Kafka
迁移的成功标准不是「框架换掉」,而是「所有 I/O 边界非阻塞化」——这也是 6.4 反复强调的判断准则。