99 Ngày Spring — Ngày 81: WebFlux nhập môn
Spring MVC dành một luồng cho mỗi request; khi request chờ I/O, luồng đó đứng chờ theo. Spring WebFlux đi theo hướng khác: một số ít luồng event loop xử lý rất nhiều kết nối, và mọi thao tác I/O đều không chặn. Đổi lại, toàn bộ chuỗi xử lý phải viết theo kiểu reactive. Bài này đặt nền: mô hình luồng, hai kiểu Mono và Flux, và tiêu chí chọn.

1. Blocking và non-blocking khác nhau ở luồng
Với mô hình thread-per-request, một nghìn request đang chờ đối tác là một nghìn luồng đang bị giữ. Với non-blocking, luồng đăng ký callback rồi quay lại phục vụ việc khác; khi dữ liệu về, event loop chạy tiếp chuỗi xử lý. Số luồng event loop mặc định xấp xỉ số nhân CPU. Thông lượng tăng khi nút thắt là thời gian chờ I/O; nếu nút thắt là CPU thì không có gì để tiết kiệm.
MVC: code tuần tự, stack trace dễ đọc, hợp với JDBC và JPA.
WebFlux: ít luồng, chịu nhiều kết nối chậm, nhưng mọi thư viện trên đường đi phải non-blocking.
2. Mono và Flux
Mono<T> phát tối đa một phần tử rồi hoàn tất hoặc báo lỗi; Flux<T> phát từ không tới nhiều phần tử. Cả hai chỉ là bản mô tả công việc: không có gì chạy cho tới khi có subscriber. Trong controller, bạn trả Mono hoặc Flux và WebFlux tự subscribe. Tự gọi subscribe() trong code nghiệp vụ thường là dấu hiệu một nhánh xử lý đã tách khỏi request.
@RestController
class PriceController {
private final WebClient client;
PriceController(WebClient.Builder builder) {
this.client = builder.baseUrl("http://pricing").build();
}
@GetMapping("/prices/{sku}")
Mono<Price> price(@PathVariable String sku) {
return client.get().uri("/api/prices/{sku}", sku)
.retrieve()
.bodyToMono(Price.class);
}
}3. Luật số một: không chặn event loop
Một lời gọi JDBC, Thread.sleep hay block() trên luồng event loop làm mọi kết nối do luồng đó phục vụ đứng theo. Nếu buộc phải gọi thư viện blocking, bọc nó trong Mono.fromCallable và chuyển sang Schedulers.boundedElastic(), một pool có giới hạn dành cho việc chặn. Đây là lối thoát cho vài điểm lẻ, không phải kiến trúc: nếu phần lớn code đều chặn, hãy ở lại MVC. BlockHound giúp phát hiện lời gọi chặn trong lúc test.
Mono<Report> report(String id) {
return Mono.fromCallable(() -> legacyClient.load(id)) // blocking call
.subscribeOn(Schedulers.boundedElastic());
}4. Khi nào cần reactive
WebFlux đáng giá khi service giữ rất nhiều kết nối chậm hoặc dài: gateway, streaming, gọi song song nhiều API, và khi mọi tầng dữ liệu đã có driver non-blocking. Nó không làm một truy vấn chậm nhanh hơn. Chi phí là thật: debug khó hơn, ThreadLocal không còn dùng như cũ, đội phải học tư duy mới. Trên Java 21, virtual thread với Spring Boot 3.2 trở lên (spring.threads.virtual.enabled=true) cho phép giữ code tuần tự mà vẫn chịu được nhiều request chờ I/O, nên hãy so cả hai bằng tải thật.
Nên cân nhắc: nhiều kết nối đồng thời chờ I/O, streaming, backpressure từ đầu tới cuối.
Chưa nên: CRUD thường với JPA, đội chưa quen reactive, nút thắt nằm ở CPU hoặc database.
Reactive là một lựa chọn về mô hình luồng, không phải nút tăng tốc. Ngày 82 đi vào các operator và backpressure, phần quyết định code reactive có đọc được hay không.
Tài liệu đối chiếu
https://docs.spring.io/spring-framework/reference/web/webflux.html
https://projectreactor.io/docs/core/release/reference/
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


