Spring WebFlux 企业级最佳实践指南
文章目录
适用于:从传统 Spring MVC / Servlet 架构逐步演进到响应式 WebFlux 的 Spring Boot 企业应用,并希望在保证稳定性的前提下获得更好的吞吐和资源利用率。
1. 响应式编程基础
1.1 Reactor 模型概览
-
核心思想:
- 基于 异步非阻塞、事件驱动 模型,通过少量线程处理大量并发请求。
- 使用 发布-订阅(Publisher-Subscriber) 协议(Reactive Streams 标准)进行数据流处理。
-
核心类型:
- Publisher:数据源(如
Mono、Flux)。 - Subscriber:消费数据的订阅者(WebFlux 框架为我们自动创建)。
- Subscription:订阅关系,支持背压(
request(n))。 - Processor:既是 Publisher 又是 Subscriber,用于桥接。
- Publisher:数据源(如
1.2 Mono 与 Flux 使用场景
-
Mono:0 或 1 个元素
-
适用场景:
- 查询单个实体:根据 ID 查询用户、获取单条视频信息。
- 写操作:新增 / 修改 / 删除操作通常返回
Mono<Void>或Mono<业务结果>。 - 只需要成功 / 失败信号:如提交表单、点赞操作结果。
-
示例:
public Mono<Video> getVideoById(Long id) { return videoRepository.findById(id); // 返回 Mono<Video> }
-
-
Flux:0 到 N 个元素(流 / 集合)
-
适用场景:
- 列表查询:分页查询视频列表、评论列表、关注列表等。
- 数据流处理:日志流、消息流、SSE(Server-Sent Events)。
-
示例:
public Flux<Video> listVideosByUser(Long userId) { return videoRepository.findByUserId(userId); // 返回 Flux<Video> }
-
1.3 常用操作符及最佳实践
-
转换类:
- map:同步纯计算,适用于 CPU 轻量逻辑。
- flatMap:异步转换,将一个元素映射为一个 Mono/Flux,再打平。
- concatMap:按顺序执行异步任务,保证顺序性。
-
错误处理:
onErrorReturn:发生错误用兜底值替代(不推荐用于关键信息)。onErrorResume:发生错误切换到备用数据源或默认逻辑。doOnError:记录日志、打点监控。
-
副作用处理:
doOnNext / doOnSuccess:记录日志、埋点、监控。doFinally:资源清理、打点(如记录总耗时)。
-
最佳实践:
- 避免在 map/flatMap 中写阻塞逻辑(如 JDBC 查询、
Thread.sleep)。 - 对外暴露 Mono/Flux,而不是
.block()得到值。 - 保持管道“冷”性质:构建链路但不要提前订阅;由 WebFlux 框架统一订阅。
- 避免在 map/flatMap 中写阻塞逻辑(如 JDBC 查询、
2. WebFlux 控制器设计
2.1 控制器风格选择
-
推荐使用注解风格 Controller(与现有项目风格一致):
@RestController @RequestMapping("/api/videos") public class VideoController { private final VideoService videoService; public VideoController(VideoService videoService) { this.videoService = videoService; } @GetMapping("/{id}") public Mono<Result<VideoVO>> getVideo(@PathVariable Long id) { return videoService.getVideoById(id) .map(video -> Result.success(toVO(video))) .switchIfEmpty(Mono.just(Result.fail("VIDEO_NOT_FOUND"))); } } -
函数式路由风格适用于:
- 较为简单的 API 网关 / 适配层。
- 配置化或 DSL 式构建路由。
2.2 请求校验与 DTO
-
使用 DTO + 校验注解 + 响应式绑定:
@PostMapping public Mono<Result<Void>> createVideo(@Valid @RequestBody Mono<CreateVideoDTO> dtoMono) { return dtoMono .flatMap(videoService::createVideo) .thenReturn(Result.success()); } -
注意:避免在控制器中进行复杂业务逻辑,将逻辑下沉至 service 层,并保持 service 也为响应式 API。
2.3 全局错误处理与异常传播
-
统一异常模型:
- 定义业务异常类:
BizException(code, message)。 - 使用
@ControllerAdvice或WebExceptionHandler进行统一处理。
- 定义业务异常类:
-
示例:基于
@RestControllerAdvice的响应式错误处理:@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public Mono<Result<Void>> handleBizException(BizException ex) { return Mono.just(Result.fail(ex.getCode(), ex.getMessage())); } @ExceptionHandler(Throwable.class) public Mono<Result<Void>> handleThrowable(Throwable ex) { // 记录日志 return Mono.just(Result.fail("INTERNAL_ERROR", "服务器繁忙,请稍后重试")); } } -
控制器内局部错误处理:
@GetMapping("/{id}") public Mono<Result<VideoVO>> getVideo(@PathVariable Long id) { return videoService.getVideoById(id) .map(video -> Result.success(toVO(video))) .switchIfEmpty(Mono.error(new BizException("VIDEO_NOT_FOUND", "视频不存在"))) .onErrorResume(BizException.class, ex -> Mono.just(Result.fail(ex.getCode(), ex.getMessage())) ); }
2.4 响应式过滤器与拦截器
-
使用
WebFilter替代 Servlet Filter:- 例如:JWT 鉴权、日志记录、TraceId 传递。
@Component public class JwtAuthenticationWebFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { // 从 Header 中解析 Token // 设置认证信息到 SecurityContext 或自定义上下文 return chain.filter(exchange); } }
3. 数据访问层集成(MyBatis Plus + WebFlux + R2DBC)
3.1 问题背景:MyBatis Plus 是阻塞式
- 现有项目中 MyBatis Plus 基于 JDBC 同步阻塞,与 WebFlux 的非阻塞模型天然不兼容。
- 直接在 WebFlux 流中调用 MyBatis 方法会引入 阻塞点,导致 Netty 事件循环线程被阻塞,吞吐下降。
3.2 三种迁移策略
-
策略一:短期过渡——在专用线程池中封装阻塞调用
- 使用
Schedulers.boundedElastic()或自定义线程池,将 MyBatis 调用放入publishOn/subscribeOn中。 - 优点:改动小,可渐进迁移;缺点:仍然是阻塞 IO,只是隔离了影响。
@Service public class VideoService { private final VideoMapper videoMapper; // MyBatis Plus Mapper public Mono<Video> getVideoById(Long id) { return Mono.fromCallable(() -> videoMapper.selectById(id)) .subscribeOn(Schedulers.boundedElastic()); } } - 使用
-
策略二:中期方案——读写分离
- 查询场景优先迁移到响应式数据源(如 R2DBC / Elasticsearch / Redis),写操作仍用 MyBatis。
- 典型场景:热门视频列表、点赞计数缓存等走响应式存储。
-
策略三:长期方案——完全响应式数据访问
- 使用 Spring Data R2DBC 替代 MyBatis Plus;
- 或者批量场景使用 R2DBC 的
DatabaseClient/R2dbcEntityTemplate实现自定义 SQL。
3.3 R2DBC 集成要点
-
依赖配置示例(以 MySQL R2DBC 为例):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency> <dependency> <groupId>io.asyncer</groupId> <artifactId>r2dbc-mysql</artifactId> <version>1.3.0</version> </dependency> -
数据源配置(
application.yml):spring: r2dbc: url: r2dbc:pool:mysql://localhost:3306/ai_story_app username: root password: your_password sql: init: mode: always -
Repository 定义示例:
@Table("video") public class Video { @Id private Long id; private String title; private Long userId; // 省略其他字段 } public interface VideoR2dbcRepository extends ReactiveCrudRepository<Video, Long> { Flux<Video> findByUserId(Long userId); } -
与现有 MyBatis Plus 共存:
- 在同一个项目中保留 MyBatis Plus Starter 和 R2DBC Starter。
- 将新的 WebFlux 控制器优先使用 R2DBC Repository;
- 旧的同步 Controller 继续使用 MyBatis Plus Mapper。
3.4 封装统一的响应式 Repository 层
-
建议:在 Service 和 Repository 之间增加一层 适配层,屏蔽底层是 MyBatis 还是 R2DBC:
public interface VideoRepository { Mono<Video> findById(Long id); Flux<Video> findByUserId(Long userId); } @Service public class VideoRepositoryImpl implements VideoRepository { private final VideoMapper videoMapper; // MyBatis Plus @Override public Mono<Video> findById(Long id) { return Mono.fromCallable(() -> videoMapper.selectById(id)) .subscribeOn(Schedulers.boundedElastic()); } @Override public Flux<Video> findByUserId(Long userId) { return Mono.fromCallable(() -> videoMapper.selectList(new LambdaQueryWrapper<Video>().eq(Video::getUserId, userId)) ) .flatMapMany(Flux::fromIterable) .subscribeOn(Schedulers.boundedElastic()); } } -
后续迁移到 R2DBC 时,仅替换
VideoRepositoryImpl的实现即可。
4. 性能优化策略
4.1 背压(Backpressure)处理
-
原则:消费者有能力告诉生产者“我一次最多能处理多少数据”。
-
常见场景:
- 下游系统(如下游服务、数据库)容量有限,无法一次处理过多请求。
-
实践建议:
-
使用
limitRate控制订阅速率:Flux<Video> videoFlux = videoService.streamAllVideos() .limitRate(100); // 每次最多请求 100 个元素 -
使用
onBackpressureBuffer / onBackpressureDrop控制上游溢出:Flux<LikeEvent> eventFlux = likeEventSource.getEvents() .onBackpressureBuffer(10_000, event -> log.warn("drop event: {}", event), BufferOverflowStrategy.DROP_OLDEST);
-
4.2 线程模型优化
-
事件循环线程:Netty 的
eventLoop,默认少量线程处理 IO; -
工作线程池:
Schedulers.boundedElastic()等用于包装阻塞任务。 -
最佳实践:
- 所有 阻塞调用(JDBC、Redis 同步客户端、文件 IO)必须封装到
boundedElastic或自定义线程池中; - 禁止在事件循环线程内执行长时间计算或阻塞操作;
- 对高 CPU 计算任务,可使用
Schedulers.parallel()。
- 所有 阻塞调用(JDBC、Redis 同步客户端、文件 IO)必须封装到
4.3 连接池与资源配置
-
R2DBC 连接池:
- 使用
r2dbc-pool,配置最大连接数、空闲连接数、超时等。
- 使用
-
Web 服务器配置:
- 通过
server.netty.*配置 Netty 的 EventLoop 线程数、HTTP 连接数等; - 根据 QPS 和 CPU 核数进行压测后再调优。
- 通过
-
外部系统调用:
-
调用外部 HTTP 服务时使用 WebClient(响应式)并配置连接池、超时、重试策略;
-
使用
retryBackoff控制重试节奏,避免雪崩:webClient.get() .uri("/external/api") .retrieve() .bodyToMono(String.class) .retryWhen(Retry.backoff(3, Duration.ofMillis(100))) .timeout(Duration.ofSeconds(3));
-
5. 缓存集成(Redis + WebFlux)
5.1 使用响应式 Redis 客户端
-
推荐使用 Spring Data Redis Reactive,其基于 Lettuce 响应式驱动。
-
依赖示例:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
5.2 响应式 RedisTemplate 使用
@Service
public class VideoCacheService {
private final ReactiveStringRedisTemplate redisTemplate;
public Mono<Video> getVideoFromCacheOrDb(Long id) {
String key = "video:" + id;
return redisTemplate.opsForValue().get(key)
.flatMap(json -> Mono.justOrEmpty(deserialize(json)))
.switchIfEmpty(
videoRepository.findById(id)
.flatMap(video -> redisTemplate.opsForValue()
.set(key, serialize(video), Duration.ofMinutes(10))
.thenReturn(video)
)
);
}
}
- 注意:
- 避免在缓存逻辑中使用同步 Redis 客户端阻塞 Reactor 线程;
- 对热点 Key 设置合理过期时间,避免缓存雪崩(可随机过期时间)。
5.3 与现有同步 Redis 代码共存
- 新的 WebFlux 代码优先使用 Reactive Redis;
- 老的同步代码可继续使用
StringRedisTemplate/RedisTemplate; - 缓存 Key 设计和 Value 序列化要统一(如 JSON + 统一前缀)。
6. 安全配置(Spring Security WebFlux)
6.1 依赖与模块区分
-
使用 WebFlux 时需引入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-webflux</artifactId> <scope>compile</scope> </dependency>实际依赖名称随 Spring Boot 版本不同会略有差异,建议直接使用
spring-boot-starter-security+ WebFlux Starter,由 BOM 管理版本。
6.2 响应式 Security 配置示例
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/public/**").permitAll()
.anyExchange().authenticated()
)
.authenticationManager(reactiveAuthenticationManager())
.securityContextRepository(securityContextRepository())
.build();
}
// 自定义 ReactiveAuthenticationManager、SecurityContextRepository 用于 JWT、Token 鉴权
}
- 注意:
- WebFlux 使用
SecurityWebFilterChain,而不是 Servlet 模式下的SecurityFilterChain; - 认证、鉴权逻辑要全部改写为响应式风格(返回
Mono<Authentication>)。
- WebFlux 使用
7. 测试策略(单元测试 & 集成测试)
7.1 控制器 / 路由测试
-
使用
WebTestClient测试 WebFlux 控制器:@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class VideoControllerTest { @Autowired private WebTestClient webTestClient; @Test void getVideo_shouldReturnSuccess() { webTestClient.get().uri("/api/videos/{id}", 1L) .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$.code").isEqualTo(0); } }
7.2 Service 层与 Reactor 流测试
-
使用 Reactor 的
StepVerifier:@Test void getVideoById_shouldReturnVideo() { Mono<Video> mono = videoService.getVideoById(1L); StepVerifier.create(mono) .expectNextMatches(video -> video.getId() == 1L) .verifyComplete(); } -
注意:
- 尽量不要在测试中使用
.block(),而是采用 StepVerifier; - 如需对超时、重试逻辑进行测试,可使用虚拟时间(
VirtualTimeScheduler)。
- 尽量不要在测试中使用
7.3 集成测试与端到端测试
- 对关键业务流程(如视频上传、点赞、评论)使用 WebTestClient 编写集成测试;
- 对响应式链路的性能与稳定性进行压测,关注:
- 99 分位响应时间;
- 吞吐(QPS);
- CPU 和内存占用;
- 线程池使用情况。
8. 监控与可观测性
8.1 指标收集(Metrics)
-
使用 Micrometer + Prometheus /其他监控后端:
- 监控请求耗时、QPS、错误率、背压发生次数、重试次数等。
-
在关键链路中埋点:
public Mono<Video> getVideoById(Long id) { Timer.Sample sample = Timer.start(meterRegistry); return repository.findById(id) .doOnSuccess(v -> sample.stop(Timer.builder("video.get.by.id") .tag("status", v != null ? "success" : "not_found") .register(meterRegistry)) ); }
8.2 日志记录
-
使用结构化日志(JSON)+ TraceId / SpanId:
- 在 WebFilter 中统一注入 TraceId 到 MDC;
- 控制器和 Service 中通过日志打印关键业务字段。
-
对于 Reactor 链路中的日志:
- 使用
log()仅限开发调试环境; - 生产环境中使用
doOnNext / doOnError+ Logger,以避免日志过多导致性能问题。
- 使用
8.3 链路追踪(Tracing)
- 使用 Spring Cloud Sleuth / OpenTelemetry,对 Reactor 链路开启 Trace:
- 确保上下文在 Reactive 流中正确传播(使用官方自动集成);
- 对跨服务调用(WebClient、消息队列)传播 Trace Header。
9. 与现有同步代码的兼容与渐进迁移
9.1 并存架构形态
-
同一应用内同时存在两套栈:
- Servlet 模式:Spring MVC + MyBatis Plus + 同步 Redis,用于传统业务接口;
- WebFlux 模式:Spring WebFlux + R2DBC / Reactive Redis,用于新业务或高并发接口。
-
端口与路径规划:
- 同一端口下区分路径,如
/api/**(同步) vs/reactive/**(响应式); - 或使用独立端口 / 独立应用,按业务拆分。
- 同一端口下区分路径,如
9.2 迁移优先级建议
-
优先迁移的场景:
- 高 QPS、IO 密集型接口(如视频列表、推荐流、点赞 / 收藏等);
- 新增业务模块(优先使用 WebFlux 架构)。
-
暂缓迁移的场景:
- 大量复杂事务逻辑、依赖多表联查的老业务;
- 强依赖 MyBatis 特性的场景(如复杂动态 SQL)。
9.3 与阻塞代码的交互
-
在 WebFlux 控制器中调用旧 Service(阻塞):
@GetMapping("/legacy/{id}") public Mono<Result<VideoVO>> getVideoLegacy(@PathVariable Long id) { return Mono.fromCallable(() -> legacyVideoService.getVideo(id)) .subscribeOn(Schedulers.boundedElastic()) .map(Result::success); } -
要点:
- 所有同步 Service 调用都应通过
fromCallable + subscribeOn包装; - 明确区分响应式 Service 与阻塞 Service 的命名和包结构,避免混用。
- 所有同步 Service 调用都应通过
10. 常见陷阱与解决方案
10.1 阻塞调用的识别与处理
-
常见阻塞点:
- MyBatis / JPA / JDBC 调用;
- 同步 Redis 客户端;
- 文件 IO(
FileInputStream、Files.readAllBytes等); - 外部 HTTP 调用使用
RestTemplate; - 使用
.block()、.toFuture().get()等显式阻塞操作。
-
识别方法:
- 代码审查:查找上述 API;
- 压测 + 线程 Dump:观察 Netty 事件循环线程是否被阻塞在数据库 / IO 调用上;
- 使用 Reactor 的
Hooks.onOperatorDebug()辅助定位(仅在测试环境使用)。
-
处理方案:
- 短期:
publishOn/subscribeOn(Schedulers.boundedElastic())包装; - 中长期:替换为响应式客户端(R2DBC、WebClient、Reactive Redis 等)。
- 短期:
10.2 .block() 滥用
-
问题:
- 在 WebFlux 请求线程中调用
.block()会直接阻塞事件循环,等同于毁掉响应式优势; - 容易造成死锁和线程饥饿。
- 在 WebFlux 请求线程中调用
-
替代方式:
- 在完全响应式链路中,始终返回 Mono/Flux,不在中间
.block(); - 在必须与同步代码交互时,仅在边界层(如旧的 MVC Controller)使用
.block(),并记录清楚。
- 在完全响应式链路中,始终返回 Mono/Flux,不在中间
10.3 内存泄漏与缓冲问题
-
潜在原因:
- 无限缓存数据(如
collectList()在大数据量场景中使用不当); - 背压处理不当,导致大量排队数据驻留内存;
- 热源(Hot Publisher)没有及时取消订阅。
- 无限缓存数据(如
-
建议:
- 大列表查询必须分页,避免一次性加载全部数据;
- 使用背压策略限制上游速度;
- 对长连接、SSE、WebSocket 等场景严格控制客户端数量和空闲超时;
- 定期利用压测和内存分析工具(如 MAT、YourKit)排查内存状况。
10.4 调试困难与错误栈信息不全
- 在生产环境避免开启
onOperatorDebug(),会有明显性能损耗; - 在测试环境使用
StepVerifier+ 日志定位问题; - 在关键链路中使用
doOnEach打印上下文信息(带业务 ID / TraceId)。
11. 总结与实施建议
-
设计原则:
- 优先保证业务稳定性,其次追求性能与资源利用率;
- 通过分层和适配器模式屏蔽底层数据访问方式,使迁移可渐进进行;
- 严格区分阻塞与非阻塞代码路径,避免“伪响应式”。
-
落地步骤建议(结合当前项目):
- 步骤 1:为新模块(如推荐流、实时互动)引入 WebFlux + WebClient,且使用
Schedulers.boundedElastic()包装现有 MyBatis 调用; - 步骤 2:为读多写少的查询链路引入 R2DBC / Reactive Redis,降低数据库压力;
- 步骤 3:逐步将高 QPS 接口重写为全响应式链路,并接入统一监控、链路追踪体系;
- 步骤 4:评估迁移收益,对低收益的老接口维持现状,以降低改造风险。
- 步骤 1:为新模块(如推荐流、实时互动)引入 WebFlux + WebClient,且使用
通过以上实践,可以在现有 Spring Boot 项目架构基础上渐进式引入 Spring WebFlux,在保证兼容性的前提下,提高系统整体吞吐、稳定性和可观测性。
更多推荐


所有评论(0)