Backend

99 Ngày Spring — Ngày 81: WebFlux nhập môn

SSite Admin
16 tháng 10, 2026 3 phút đọc 4 lượt xem
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.

WebFlux nhập mô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/

S

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.

Bài viết liên quan

99 Ngày Java — Ngày 81: var và Type Inference

var suy kiểu ở đâu, kiểu nào được chọn, những chỗ không dùng được và guideline để code vẫn dễ đọc.

16 thg 10, 20263 phút4
99 Ngày Spring — Ngày 80: Saga và Tính nhất quán Dữ liệu

Transactional outbox, saga choreography/orchestration, compensation và consumer idempotent.

15 thg 10, 20262 phút5
99 Ngày Java — Ngày 80: Memory Leak trong Java

Rò rỉ do cache, listener, ThreadLocal và classloader; tìm retained size, GC roots rồi sửa nguồn giữ.

15 thg 10, 20262 phút9