🎯 我踩过的坑
去年我们团队要做一个实时消息推送系统,要求支持 10 万并发长连接。开始用的 Spring MVC + Tomcat,测试时 1000 连接就崩了,Tomcat 线程池直接打满。
后来换成 WebFlux + Netty,同样的机器配置,10 万连接稳稳的。但高兴得太早,上线后发现 CPU 使用率比原来高了 30%。排查发现是有个同事在响应式链里写了阻塞代码。
还有个更坑的:我们有个批量查询接口,用 WebFlux 重写后,响应时间反而变长了。查了三天,发现是因为没处理好背压,数据量大的时候内存暴涨。
这些经历让我明白:不懂原理的技术选型就是耍流氓,WebFlux 不是银弹,用错了比不用还惨。
✨ 摘要
Spring WebFlux 是响应式 Web 框架,基于 Reactor 和 Netty 实现。本文深度对比 Servlet 阻塞模型与响应式非阻塞模型的差异,从线程模型、IO 处理、内存管理三个维度解析性能关键。通过源码分析揭示 WebFlux 的请求处理流程、编解码机制、错误处理策略。结合压测数据和实战案例,提供 WebFlux 的正确使用姿势和性能优化指南。
1. 别急着用 WebFlux,先搞清楚这两个问题
1.1 什么时候该用 WebFlux?
很多人盲目追新技术,我总结了几个判断标准:
适合 WebFlux 的场景:
- ✅ 高并发 IO 密集型应用(API 网关、消息推送)
- ✅ 实时数据流处理(股票行情、实时监控)
- ✅ 需要大量长连接的场景(WebSocket、SSE)
- ✅ 微服务间的非阻塞调用
不适合 WebFlux 的场景:
- ❌ CPU 密集型计算(大数据分析、复杂算法)
- ❌ 已有大量阻塞代码的遗留系统
- ❌ 团队对响应式编程不熟悉
- ❌ 简单 CRUD 应用,并发不高
1.2 性能神话 vs 现实
先看个真实的性能对比,这是我们在 4 核 8G 机器上的测试结果:
| 场景 | Spring MVC (Tomcat) | WebFlux (Netty) | 提升 |
|---|
| 100 并发,简单查询 | 3200 QPS | 3500 QPS | 9.4% |
| 1000 并发,简单查询 | 2800 QPS | 5200 QPS | 85.7% |
| 100 并发,IO 密集 | 1200 QPS | 3800 QPS | 216.7% |
| 内存占用(1000 连接) | 450MB | 320MB | 28.9% |
看到没?只有在高并发和 IO 密集型场景下,WebFlux 才有明显优势。
2. 线程模型:一个线程 vs 少量线程
2.1 Servlet 的"一个请求一个线程"
这是 Servlet 最经典的模型,也是最大的瓶颈:
public class Http11Processor implements Runnable {
public void run() {
try {
parseRequest();
servlet.service(request, response);
writeResponse();
} catch (Exception e) {
handleError(e);
}
}
}
@RestController
public class UserController {
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElse(null);
}
}
代码清单 1:Servlet 线程模型
问题:线程在等待 IO 时被阻塞,什么也干不了,浪费资源。
2.2 WebFlux 的"事件循环"
WebFlux 用了完全不同的思路:
public class NioEventLoop extends SingleThreadEventLoop {
protected void run() {
for (;;) {
try {
int readyChannels = selector.select();
if (readyChannels > 0) {
processSelectedKeys();
}
runAllTasks();
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
@RestController
public class UserController {
@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.subscribeOn(Schedulers.boundedElastic());
}
}
代码清单 2:WebFlux 事件循环模型
响应式模型的工作方式:
关键点:事件循环线程只做 IO 调度,耗时操作交给工作线程池,处理完回调。
3. 请求处理流程对比
3.1 Spring MVC 的请求处理链
看 Spring MVC 是怎么处理请求的:
public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(request, response)) {
return;
}
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
mappedHandler.applyPostHandle(request, response, mv);
processDispatchResult(request, response, mappedHandler, mv, null);
}
}
代码清单 3:Spring MVC 请求处理
看到没?线程大部分时间在等待,啥也干不了。
3.2 WebFlux 的请求处理链
WebFlux 的处理方式完全不同:
public class DispatcherHandler implements WebHandler {
public Mono<Void> handle(ServerWebExchange exchange) {
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
HandlerAdapter adapter = getHandlerAdapter(handler);
return adapter.handle(exchange, handler);
}
}
代码清单 4:WebFlux 请求处理
关键优势:事件循环线程永不阻塞,一个线程能处理成千上万的连接。
4. 内存管理:堆内 vs 堆外
4.1 Servlet 的内存模型
Servlet 使用的是传统的堆内存:
public class UserController {
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
byte[] bytes = file.getBytes();
processFile(bytes);
return "success";
}
}
内存分配情况:
- 每个请求独立的内存空间
- 大文件上传时内存暴涨
- GC 压力大,容易 Full GC
4.2 WebFlux 的零拷贝与堆外内存
WebFlux 利用 Netty 的零拷贝和堆外内存:
@RestController
public class FileController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<String> upload(FilePart file) {
return file.content()
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
.collectList()
.flatMap(chunks -> {
return processChunks(chunks);
});
}
@GetMapping(value = "/download/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Mono<Resource> download(@PathVariable String id) {
return Mono.fromCallable(() -> new FileSystemResource("/data/files/" + id));
}
}
代码清单 5:WebFlux 文件处理
性能测试(上传 100MB 文件,100 并发):
| 指标 | Spring MVC | WebFlux | 对比 |
|---|
| 内存峰值 | 2.5GB | 800MB | 减少 68% |
| GC 时间 | 4.2s | 0.8s | 减少 81% |
| 平均耗时 | 3.4s | 1.8s | 减少 47% |
5. 连接处理:短连接 vs 长连接
5.1 Servlet 的短连接瓶颈
Tomcat 默认使用短连接,每个请求创建新连接:
public class Http11Processor {
public void action(ActionCode actionCode, Object param) {
switch (actionCode) {
case CLOSE:
endRequest();
break;
}
}
private void endRequest() {
inputBuffer.recycle();
outputBuffer.recycle();
if (!isKeepAlive()) {
closeSocket();
}
}
}
问题:
- 高并发时大量连接创建/销毁开销
- 端口资源有限
- 握手开销大(SSL 握手更甚)
5.2 WebFlux 的长连接优势
Netty 天生支持长连接:
@Configuration
public class Http2Config {
@Bean
public NettyServerCustomizer nettyServerCustomizer() {
return httpServer -> httpServer
.protocol(HttpProtocol.H2, HttpProtocol.HTTP11)
.compress(true)
.http2Settings(settings -> settings.maxConcurrentStreams(1000L));
}
}
@RestController
public class ChatController {
@MessageMapping("chat")
public Flux<Message> chat(Flux<Message> inbound) {
return inbound
.doOnNext(message -> log.info("收到消息:{}", message))
.doOnComplete(() -> log.info("连接关闭"));
}
}
连接数对比测试(10 万并发连接):
现实案例:我们做的消息推送系统,用 Tomcat 最多支撑 5000 长连接,用 Netty 轻松 10 万连接。
6. 错误处理:同步 vs 异步
6.1 Servlet 的错误处理
Servlet 的错误处理是同步的:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(500)
.body("错误:" + e.getMessage());
}
}
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<String> handleNPE(NullPointerException e) {
String msg = e.getMessage();
return ResponseEntity.status(500).body(msg);
}
6.2 WebFlux 的错误处理
WebFlux 的错误处理是响应式的:
@ControllerAdvice
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
return Mono.fromRunnable(() -> {
log.error("处理请求异常", ex);
})
.then(Mono.defer(() -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
byte[] bytes = ("错误:" + ex.getMessage()).getBytes(UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}))
.onErrorResume(e -> {
log.error("错误处理器异常", e);
return Mono.empty();
});
}
}
代码清单 6:WebFlux 错误处理
7. 实战:构建 API 网关
7.1 需求分析
我们需要一个高性能 API 网关,要求:
- 支持 5 万 QPS
- 动态路由
- 熔断降级
- 限流鉴权
- 实时监控
7.2 Spring Cloud Gateway 实现
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user_service", r -> r
.path("/api/users/**")
.filters(f -> f
.addRequestHeader("X-Request-ID", UUID.randomUUID().toString())
.circuitBreaker(config -> config
.setName("userService")
.setFallbackUri("forward:/fallback/user"))
.requestRateLimiter(config -> config
.setRateLimiter(redisRateLimiter()))
.retry(config -> config
.setRetries(3)
.setStatuses(HttpStatus.SERVICE_UNAVAILABLE)))
.uri("lb://user-service"))
.build();
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(100, 200, 1);
}
}
@Component
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest()
.getHeaders().getFirst("Authorization");
(token == ) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().setComplete();
}
validateToken(token)
.flatMap(valid -> {
(valid) {
chain.filter(exchange);
} {
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().setComplete();
}
});
}
Mono<Boolean> {
webClient.post()
.uri()
.bodyValue( (token))
.retrieve()
.bodyToMono(TokenResponse.class)
.map(TokenResponse::isValid)
.timeout(Duration.ofSeconds())
.onErrorReturn();
}
}
代码清单 7:Spring Cloud Gateway 配置
7.3 性能压测结果
测试环境:
- 4 核 8GB * 3 节点
- 后端服务:Spring Boot 2.7
- 压测工具:wrk
测试结果:
| 场景 | Spring Cloud Zuul | Spring Cloud Gateway | 提升 |
|---|
| 100 并发,简单路由 | 4200 QPS | 18500 QPS | 340% |
| 1000 并发,复杂过滤 | 1200 QPS | 8500 QPS | 608% |
| 长连接支持 | 差 | 优秀 | - |
| 内存占用 | 650MB | 320MB | 51% |
8. 常见问题与解决方案
8.1 阻塞代码问题
这是 WebFlux 最常见的问题:
@GetMapping("/blocking")
public Mono<String> blocking() {
return Mono.fromCallable(() -> {
Thread.sleep(1000);
return "done";
});
}
@GetMapping("/non-blocking")
public Mono<String> nonBlocking() {
return Mono.fromCallable(() -> {
Thread.sleep(1000);
return "done";
})
.subscribeOn(Schedulers.boundedElastic());
}
@PostConstruct
public void init() {
BlockHound.install();
}
8.2 内存泄漏排查
响应式编程容易内存泄漏:
@RestController
public class MetricsController {
@GetMapping("/metrics/memory")
public Mono<Map<String, Object>> memoryMetrics() {
return Mono.fromCallable(() -> {
Runtime runtime = Runtime.getRuntime();
Map<String, Object> metrics = new HashMap<>();
metrics.put("totalMemory", runtime.totalMemory());
metrics.put("freeMemory", runtime.freeMemory());
metrics.put("maxMemory", runtime.maxMemory());
metrics.put("directMemory", PlatformDependent.usedDirectMemory());
return metrics;
});
}
}
8.3 调试技巧
Hooks.onOperatorDebug();
flux.checkpoint("source")
.map(...)
.checkpoint("afterMap")
.subscribe();
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "webflux-demo");
}
@Bean
public WebFilter slowRequestFilter() {
return (exchange, chain) -> {
long start = System.currentTimeMillis();
return chain.filter(exchange)
.doFinally(signal -> {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) {
log.warn("慢请求:{} {}, 耗时:{}ms", exchange.getRequest().getMethod(), exchange.getRequest().getPath(), cost);
}
});
};
}
9. 性能优化指南
9.1 线程池优化
spring:
webflux:
client:
max-connections: 1000
max-memory-size: 10MB
server:
netty:
connection-timeout: 30s
idle-timeout: 60s
reactor:
schedulers:
default-pool-size: 4
bounded-elastic:
max-threads: 200
queue-size: 10000
ttl: 60
9.2 内存优化
@Bean
public NettyServerCustomizer nettyServerCustomizer() {
return server -> server
.metrics(true, () -> new MicrometerChannelMetricsRecorder("server", "http"))
.accessLog(true)
.runOn(LoopResources.create("webflux", 4, true))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
@Scheduled(fixedRate = 5000)
public void monitorDirectMemory() {
long used = PlatformDependent.usedDirectMemory();
long max = PlatformDependent.maxDirectMemory();
if (used > max * 0.8) {
log.warn("直接内存使用过高:{}/{}", used, max);
}
}
9.3 背压处理
@GetMapping("/stream")
public Flux<Data> streamData() {
return dataService.getDataStream()
.onBackpressureBuffer(1000, BufferOverflowStrategy.DROP_OLDEST)
.doOnNext(data -> metrics.recordDataProcessed())
.delayElements(Duration.ofMillis(10))
.timeout(Duration.ofSeconds(30))
.doOnError(TimeoutException.class, e -> log.error("数据流超时"));
}
10. 迁移指南:从 Servlet 到 WebFlux
10.1 渐进式迁移策略
不要一次性重写整个系统:
10.2 兼容性处理
@Configuration
public class HybridConfig {
@Bean
public RouterFunction<ServerResponse> webFluxRoutes() {
return RouterFunctions.route()
.GET("/reactive/users", this::getUsers)
.build();
}
@RestController
@RequestMapping("/mvc")
public class MvcController {
@GetMapping("/users")
public List<User> getUsers() {
return userService.getUsers();
}
}
}
@Service
public class HybridService {
public Mono<List<User>> getUsersReactive() {
return Mono.fromCallable(() ->
blockingUserService.getUsers()
)
.subscribeOn(Schedulers.boundedElastic())
.flatMapMany(Flux::fromIterable)
.collectList();
}
}
11. 最后的话
WebFlux 不是 Spring MVC 的替代品,它们是解决不同问题的工具。就像螺丝刀和锤子,用对了事半功倍,用错了事倍功半。
我见过太多团队在技术选型上犯错:有的在简单 CRUD 系统上用 WebFlux,复杂度增加但收益不大;有的在高并发场景下死守 Servlet,性能上不去还怪硬件不行。
记住:技术是为业务服务的,选型要结合业务场景、团队能力、运维成本综合考虑。
📚 推荐阅读
官方文档
- Spring WebFlux 官方文档 - 最权威的参考
- Project Reactor 文档 - 响应式编程基础
源码学习
- Spring WebFlux 源码 - 直接看源码
- Netty 源码 - 网络层实现
最佳实践
- Spring Cloud Gateway 指南 - API 网关实战
- 响应式系统设计 - 响应式宣言
性能工具
- Micrometer 监控 - 响应式应用监控
- BlockHound - 阻塞调用检测
最后建议:不要因为 WebFlux"时髦"就用它。先分析你的业务场景,做个小规模 POC 测试,用数据说话。记住:先验证,后落地;先小范围,后推广。