从阻塞模型到响应式模型的架构抉择
Spring Boot 3.x WebFlux基于Project Reactor实现非阻塞I/O,单机可支撑数万并发连接,而传统Servlet容器(Tomcat)默认线程池200线程,每个阻塞调用占用一个线程,QPS达到几千时线程池即告竭。后端开发中,高并发I/O密集型场景(网关聚合、消息推送、实时数据流)是WebFlux的典型应用场景。但响应式编程的学习曲线陡峭,调试困难,并非所有项目都适合迁移。
WebFlux核心概念与Reactor操作符
// Reactor两大发布者类型
// Mono: 0或1个元素的异步序列
// Flux: 0或N个元素的异步序列
// 基础创建
Mono.just("hello");
Mono.fromCallable(() -> db.queryById(1));
Flux.fromIterable(list);
Flux.interval(Duration.ofSeconds(1));
// 关键操作符
// map - 同步转换
Mono.just(1).map(x -> x * 2) // 2
// flatMap - 异步转换(核心操作符)
// 每个元素映射为Mono/Flux并合并
userIds.flatMap(id -> userRepository.findById(id))
// filter - 过滤
flux.filter(item -> item.isActive())
// zip - 并行组合多个Mono
Mono.zip(
orderService.getOrder(orderId),
userService.getUser(userId),
(order, user) -> new OrderDTO(order, user)
)
// buffer - 批量聚合
flux.buffer(100) // 每100条一批处理
// onErrorResume - 错误恢复
mono.onErrorResume(e -> {
log.error("Fallback triggered", e);
return Mono.just(defaultValue);
})
// retry - 重试
mono.retry(3) // 最多重试3次
mono.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(10))
.jitter(0.5))
WebFlux服务层开发实战
// 用户服务 - 完全非阻塞
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepo;
private final CacheService cache;
private final EventPublisher eventPublisher;
public Mono<UserDTO> getUser(Long id) {
return cache.get("user:" + id, UserDTO.class)
.switchIfEmpty(
userRepo.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("用户不存在")))
.map(this::toDTO)
.flatMap(dto -> cache.set("user:" + id, dto, 300).thenReturn(dto))
);
}
public Mono<PageResult<UserDTO>> listUsers(PageRequest page) {
return Mono.zip(
userRepo.count(),
userRepo.findAllByPage(page)
.map(this::toDTO)
.collectList(),
(total, list) -> new PageResult<>(list, total, page)
);
}
public Mono<UserDTO> createUser(CreateUserReq req) {
return userRepo.findByName(req.getName())
.hasElement()
.flatMap(exists -> {
if (exists) return Mono.error(
new ConflictException("用户名已存在"));
return userRepo.save(toEntity(req))
.flatMap(saved -> eventPublisher
.publish("user.created", saved)
.thenReturn(toDTO(saved)));
});
}
}
// 非阻塞Repository(R2DBC)
@Repository
public interface UserRepository
extends ReactiveCrudRepository<User, Long> {
Mono<User> findByName(String name);
Flux<User> findAllByPage(Pageable pageable);
Mono<Long> count();
}
WebFlux Controller与请求校验
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public Mono<ResponseEntity<UserDTO>> getUser(
@PathVariable Long id) {
return userService.getUser(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<UserDTO>> createUser(
@Valid @RequestBody Mono<CreateUserReq> reqMono) {
return reqMono.flatMap(req ->
userService.createUser(req)
.map(dto -> ResponseEntity
.status(HttpStatus.CREATED)
.body(dto))
);
}
@GetMapping
public Mono<PageResult<UserDTO>> listUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
return userService.listUsers(
PageRequest.of(page - 1, size));
}
}
// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public Mono<ResponseEntity<ErrorResp>> handleNotFound(NotFoundException e) {
return Mono.just(ResponseEntity.status(404)
.body(new ErrorResp(e.getMessage())));
}
@ExceptionHandler(ConflictException.class)
public Mono<ResponseEntity<ErrorResp>> handleConflict(ConflictException e) {
return Mono.just(ResponseEntity.status(409)
.body(new ErrorResp(e.getMessage())));
}
@ExceptionHandler(WebExchangeBindException.class)
public Mono<ResponseEntity<ErrorResp>> handleValidation(WebExchangeBindException e) {
String msg = e.getFieldErrors().stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.collect(Collectors.joining("; "));
return Mono.just(ResponseEntity.badRequest()
.body(new ErrorResp(msg)));
}
}
阻塞代码迁移策略与避坑指南
// ❌ 致命错误:在WebFlux中调用阻塞API
public Mono<UserDTO> getUser(Long id) {
// 这会阻塞EventLoop线程,整个服务性能崩塌
User user = jdbcUserRepo.findById(id);
return Mono.just(toDTO(user));
}
// ✅ 方案1:使用subscribeOn将阻塞调用调度到弹性线程池
public Mono<UserDTO> getUser(Long id) {
return Mono.fromCallable(() -> jdbcUserRepo.findById(id))
.subscribeOn(Schedulers.boundedElastic())
.map(this::toDTO);
}
// ✅ 方案2:对遗留系统包装适配器
@Component
public class LegacyUserAdapter {
private final JdbcUserRepo repo;
public Mono<UserDTO> findById(Long id) {
return Mono.fromCallable(() -> repo.findById(id))
.subscribeOn(Schedulers.boundedElastic())
.timeout(Duration.ofSeconds(5))
.map(this::toDTO);
}
}
// 配置:限制弹性线程池大小,防止阻塞调用耗尽线程
// application.yml
spring:
reactor:
netty:
ioWorkerCount: 8 # I/O线程数,通常=CPU核数
ioSelectCount: 2 # selector线程数
lifecycle:
timeout-per-shutdown-phase: 30s
从阻塞模型迁移到WebFlux不是简单的API替换,而是编程范式的转换。迁移时采用渐进式策略:新模块直接用WebFlux编写,遗留模块用boundedElastic线程池桥接。切记不要在Reactor链中混入阻塞调用,这是WebFlux性能劣化的首要原因。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot3x-xiang-ying-shi-bian-cheng-webflux-shi-zhan/