paint-brush
Giới thiệu về mô hình luồng Spring WebFluxby@vladimirf
13,753
13,753

Giới thiệu về mô hình luồng Spring WebFlux

pring WebFlux là một khung web phản ứng, không chặn, sử dụng thư viện Reactor để triển khai lập trình phản ứng trong Java. Mô hình luồng của WebFlux khác với mô hình luồng theo yêu cầu truyền thống được sử dụng trong nhiều khung web đồng bộ. WebFlux sử dụng mô hình hướng sự kiện, không chặn, trong đó một số lượng nhỏ luồng có thể xử lý một số lượng lớn yêu cầu. Điều này cho phép luồng tiếp tục xử lý các yêu cầu khác trong khi các tác vụ được thực thi ở chế độ nền. Sử dụng bộ lập lịch song song có thể cải thiện hiệu suất và khả năng mở rộng bằng cách cho phép nhiều tác vụ được thực thi đồng thời trên các luồng khác nhau.
featured image - Giới thiệu về mô hình luồng Spring WebFlux
Vladimir Filipchenko HackerNoon profile picture
0-item

Spring WebFlux là một khung web phản ứng, không chặn để xây dựng các ứng dụng web hiện đại, có thể mở rộng trong Java. Nó là một phần của Spring Framework và nó sử dụng thư viện Reactor để triển khai lập trình phản ứng trong Java.


Với WebFlux, bạn có thể xây dựng các ứng dụng web có khả năng mở rộng, hiệu suất cao, có thể xử lý một số lượng lớn các yêu cầu và luồng dữ liệu đồng thời. Nó hỗ trợ nhiều trường hợp sử dụng, từ API REST đơn giản đến truyền dữ liệu theo thời gian thực và các sự kiện do máy chủ gửi.


Spring WebFlux cung cấp một mô hình lập trình dựa trên các luồng phản ứng, cho phép bạn kết hợp các hoạt động không đồng bộ và không chặn thành một đường dẫn các giai đoạn xử lý dữ liệu. Nó cũng cung cấp một bộ tính năng và công cụ phong phú để xây dựng các ứng dụng web phản ứng, bao gồm hỗ trợ truy cập dữ liệu phản ứng, bảo mật phản ứng và thử nghiệm phản ứng.


Từ tài liệu chính thức của Spring :

Thuật ngữ “phản ứng” đề cập đến các mô hình lập trình được xây dựng xung quanh việc phản ứng với sự thay đổi — các thành phần mạng phản ứng với các sự kiện I/O, bộ điều khiển giao diện người dùng phản ứng với các sự kiện chuột và những thứ khác. Theo nghĩa đó, không chặn là phản ứng, bởi vì, thay vì bị chặn, chúng tôi hiện đang ở chế độ phản ứng với các thông báo khi hoạt động hoàn tất hoặc dữ liệu có sẵn.

mô hình luồng

Một trong những tính năng cốt lõi của lập trình phản ứng là mô hình luồng của nó, khác với mô hình luồng theo yêu cầu truyền thống được sử dụng trong nhiều khung web đồng bộ.


Trong mô hình truyền thống, một luồng mới được tạo để xử lý từng yêu cầu đến và luồng đó bị chặn cho đến khi yêu cầu được xử lý. Điều này có thể dẫn đến các vấn đề về khả năng mở rộng khi xử lý khối lượng lớn yêu cầu, vì số lượng luồng cần thiết để xử lý yêu cầu có thể trở nên rất lớn và việc chuyển ngữ cảnh luồng có thể trở thành nút cổ chai.


Ngược lại, WebFlux sử dụng mô hình hướng sự kiện, không chặn, trong đó một số lượng nhỏ luồng có thể xử lý một số lượng lớn yêu cầu. Khi một yêu cầu đến, nó sẽ được xử lý bởi một trong các luồng có sẵn, sau đó ủy quyền xử lý thực tế cho một tập hợp các tác vụ không đồng bộ. Các tác vụ này được thực thi theo cách không chặn, cho phép luồng tiếp tục xử lý các yêu cầu khác trong khi các tác vụ được thực thi trong nền.


Trong Spring WebFlux (và các máy chủ không chặn nói chung), giả định rằng các ứng dụng không chặn. Do đó, các máy chủ không chặn sử dụng một nhóm luồng nhỏ, có kích thước cố định (nhân viên vòng lặp sự kiện) để xử lý các yêu cầu.


Mô hình luồng đơn giản hóa của Bộ chứa Servlet cổ điển trông như sau:

Mặc dù quá trình xử lý yêu cầu WebFlux hơi khác một chút:

Dưới mui xe

Hãy tiếp tục và xem những gì đằng sau lý thuyết sáng bóng.

Chúng tôi cần một ứng dụng khá tối giản được tạo bởi Spring Initializr . Mã có sẵn trong repo GitHub .


Tất cả các chủ đề liên quan đến luồng đều phụ thuộc rất nhiều vào CPU. Thông thường, số luồng xử lý xử lý các yêu cầu có liên quan đến số lượng lõi CPU . Đối với mục đích giáo dục, bạn có thể dễ dàng thao tác số lượng luồng trong nhóm bằng cách giới hạn CPU khi chạy bộ chứa Docker:

 docker run --cpus=1 -d --rm --name webflux-threading -p 8081:8080 local/webflux-threading

Nếu bạn vẫn thấy nhiều chuỗi trong một nhóm - điều đó không sao cả. Có thể có các giá trị mặc định do WebFlux đặt.

Ứng dụng của chúng tôi là một thầy bói đơn giản. Bằng cách gọi /karma điểm cuối, bạn sẽ nhận được 5 bản ghi với balanceAdjustment . Mỗi lần điều chỉnh là một số nguyên đại diện cho một nghiệp được ban cho bạn. Vâng, chúng tôi rất hào phóng vì ứng dụng chỉ tạo ra các số dương. Không còn xui xẻo nữa!

xử lý mặc định

Hãy bắt đầu với một ví dụ rất cơ bản. Phương thức điều khiển tiếp theo trả về một Thông lượng chứa 5 phần tử nghiệp.


 @GetMapping("/karma") public Flux<Karma> karma() { return prepareKarma() .map(Karma::new) .log(); } private Flux<Integer> prepareKarma() { Random random = new Random(); return Flux.fromStream( Stream.generate(() -> random.nextInt(10)) .limit(5)); }


log là một điều quan trọng ở đây. Nó quan sát tất cả các tín hiệu Luồng phản ứng và theo dõi chúng vào các bản ghi dưới cấp INFO.


Đầu ra nhật ký trên curl localhost:8081/karma như sau:


Như chúng ta có thể thấy, quá trình xử lý đang diễn ra trên nhóm luồng IO. Tên chủ đề ctor-http-nio-2 là viết tắt của reactor-http-nio-2 . Các tác vụ được thực thi ngay lập tức trên một chuỗi đã gửi chúng. Reactor không thấy bất kỳ hướng dẫn nào để lên lịch cho chúng trên một nhóm khác.

Trì hoãn và xử lý song song

Hoạt động tiếp theo sẽ trì hoãn mỗi phần tử phát ra 100 mili giây (còn gọi là mô phỏng cơ sở dữ liệu)


 @GetMapping("/delayedKarma") public Flux<Karma> delayedKarma() { return karma() .delayElements(Duration.ofMillis(100)); }


Chúng ta không cần thêm phương thức log ở đây vì nó đã được khai báo trong lệnh gọi karma() ban đầu.


Trong nhật ký, chúng ta có thể thấy hình ảnh tiếp theo:


Lần này chỉ nhận được phần tử đầu tiên trên luồng IO reactor-http-nio-4 . Việc xử lý 4 phần còn lại được dành riêng cho nhóm luồng song parallel .


Javadoc của delayElements xác nhận điều này:

Tín hiệu bị trì hoãn và tiếp tục trên Bộ lập lịch mặc định song song


Bạn có thể đạt được hiệu quả tương tự mà không bị chậm trễ bằng cách chỉ định .subscribeOn(Schedulers.parallel()) ở bất kỳ đâu trong chuỗi cuộc gọi.


Sử dụng bộ lập lịch parallel có thể cải thiện hiệu suất và khả năng mở rộng bằng cách cho phép nhiều tác vụ được thực thi đồng thời trên các luồng khác nhau, điều này có thể sử dụng tốt hơn tài nguyên CPU và xử lý một số lượng lớn yêu cầu đồng thời.


Tuy nhiên, nó cũng có thể làm tăng độ phức tạp của mã và mức sử dụng bộ nhớ, đồng thời có khả năng dẫn đến cạn kiệt nhóm luồng nếu vượt quá số lượng luồng công nhân tối đa. Do đó, quyết định sử dụng nhóm luồng parallel phải dựa trên các yêu cầu cụ thể và sự đánh đổi của ứng dụng.


Chuỗi con

Bây giờ chúng ta hãy xem một ví dụ phức tạp hơn. Mã vẫn còn khá đơn giản và dễ hiểu, nhưng đầu ra thú vị hơn nhiều.


Chúng tôi sẽ sử dụng flatMap và làm cho một thầy bói trở nên công bằng hơn. Đối với mỗi phiên bản Karma, nó sẽ nhân số điều chỉnh ban đầu lên 10 và tạo ra các điều chỉnh ngược lại, tạo ra một giao dịch cân bằng bù đắp cho giao dịch ban đầu một cách hiệu quả.


 @GetMapping("/fairKarma") public Flux<Karma> fairKarma() { return delayedKarma() .flatMap(this::makeFair); } private Flux<Karma> makeFair(Karma original) { return Flux.just(new Karma(original.balanceAdjustment() * 10), new Karma(original.balanceAdjustment() * -10)) .subscribeOn(Schedulers.boundedElastic()) .log(); }


Như bạn thấy, Flux makeFair's phải được đăng ký vào nhóm chủ đề boundedElastic . Hãy kiểm tra những gì chúng ta có trong nhật ký của hai Karma đầu tiên:


  1. Lò phản ứng đăng ký phần tử đầu tiên với balanceAdjustment=9 trên luồng IO


  2. Sau đó, nhóm boundedElastic hoạt động dựa trên sự công bằng của Karma bằng cách đưa ra các điều chỉnh 90-90 trên luồng boundedElastic-1


  3. Các phần tử sau phần tử đầu tiên được đăng ký trên nhóm luồng song song (vì chúng tôi vẫn có delayedElements trong chuỗi)


Trình lập lịch trình boundedElastic là gì ?

Đây là một nhóm luồng tự động điều chỉnh số lượng luồng công nhân dựa trên khối lượng công việc. Nó được tối ưu hóa cho các tác vụ liên kết I/O, chẳng hạn như truy vấn cơ sở dữ liệu và yêu cầu mạng, đồng thời được thiết kế để xử lý một số lượng lớn các tác vụ tồn tại trong thời gian ngắn mà không tạo quá nhiều luồng hoặc lãng phí tài nguyên.


Theo mặc định, nhóm luồng boundedElastic kích thước tối đa bằng số lượng bộ xử lý khả dụng nhân với 10, nhưng bạn có thể định cấu hình nó để sử dụng kích thước tối đa khác nếu cần


Bằng cách sử dụng nhóm luồng không đồng bộ như boundedElastic , bạn có thể giảm tải các tác vụ cho các luồng riêng biệt và giải phóng luồng chính để xử lý các yêu cầu khác. Bản chất có giới hạn của nhóm luồng có thể ngăn chặn tình trạng thiếu luồng và sử dụng tài nguyên quá mức, trong khi tính linh hoạt của nhóm cho phép nó điều chỉnh động số lượng luồng công nhân dựa trên khối lượng công việc.


Các loại thread pool khác

Có hai loại nhóm khác được cung cấp bởi lớp Trình lập lịch biểu sẵn dùng, chẳng hạn như:


  • single : Đây là ngữ cảnh thực thi tuần tự, đơn luồng được thiết kế để thực thi đồng bộ. Nó rất hữu ích khi bạn cần đảm bảo rằng một tác vụ được thực hiện theo thứ tự và không có hai tác vụ nào được thực hiện đồng thời.


  • immediate : Đây là một triển khai tầm thường, không cần thao tác của bộ lập lịch thực hiện ngay các tác vụ trên luồng đang gọi mà không cần bất kỳ chuyển đổi luồng nào.


Phần kết luận

Mô hình phân luồng trong Spring WebFlux được thiết kế không chặn và không đồng bộ, cho phép xử lý hiệu quả một số lượng lớn yêu cầu với mức sử dụng tài nguyên tối thiểu. Thay vì dựa vào các luồng chuyên dụng trên mỗi kết nối, WebFlux sử dụng một số lượng nhỏ các luồng vòng lặp sự kiện để xử lý các yêu cầu đến và phân phối công việc cho các luồng công nhân từ các nhóm luồng khác nhau.


Tuy nhiên, điều quan trọng là phải chọn đúng nhóm luồng cho trường hợp sử dụng của bạn để tránh tình trạng thiếu luồng và đảm bảo sử dụng hiệu quả tài nguyên hệ thống.