微服務專題04-Spring WebFlux 原理

前言

前面的章節我們講了REST。本節,繼續微服務專題的內容分享,共計16小節,分別是:

  • 微服務專題01-Spring Application
  • 微服務專題02-Spring Web MVC 視圖技術
  • 微服務專題03-REST
  • 微服務專題04-Spring WebFlux 原理
  • 微服務專題05-Spring WebFlux 運用
  • 微服務專題06-雲原生應用(Cloud Native Applications)
  • 微服務專題07-Spring Cloud 配置管理
  • 微服務專題08-Spring Cloud 服務發現
  • 微服務專題09-Spring Cloud 負載均衡
  • 微服務專題10-Spring Cloud 服務熔斷
  • 微服務專題11-Spring Cloud 服務調用
  • 微服務專題12-Spring Cloud Gateway
  • 微服務專題13-Spring Cloud Stream (上)
  • 微服務專題14-Spring Cloud Bus
  • 微服務專題15-Spring Cloud Stream 實現
  • 微服務專題16-Spring Cloud 整體回顧

本節內容重點爲:

  • Reactive 原理:理解 Reactive 本質原理,解開其中的奧祕

  • WebFlux 使用場景:介紹 WebFlux 與 Spring Web MVC 的差異,WebFlux 真實的使用場景

  • WebFlux 整體架構:介紹 WebFlux、Netty 與 Reactor 之間的關係,對於 Spring Web MVC 架構深入理解

Reactive 原理

關於Spring WebFlux的基本情況這裏不再贅述,與Spring MVC不同,它不需要Servlet API,完全異步非阻塞, 並通過Reactor項目實現Reactive Streams規範。 並且可以在諸如Netty,Undertow和Servlet 3.1+容器的服務器上運行。

關於 Reactive 的一些講法

其中筆者挑選了以下三種出鏡率最高的講法:

Q:Reactive 是異步非阻塞編程(錯誤)
A:Reactive 是同步/異步非阻塞編程

Q: Reactive 能夠提升程序性能
A:大多數情況是沒有的,少數可能能會,參考測試用例地址:https://blog.ippon.tech/spring-5-webflux-performance-tests/

Q: Reactive 解決傳統編程模型遇到的困境
A: 也是錯的,傳統困境不需,也不能被 Reactive

傳統編程模型中的某些困境

Reactor 認爲阻塞可能是浪費的

http://projectreactor.io/docs/core/release/reference/#_blocking_can_be_wasteful
將以上 Reactor 觀點歸納如下,它認爲:

  • 阻塞導致性能瓶頸和浪費資源
  1. 任何代碼都是阻塞(指令是串行)
  2. 非阻塞從實現來說,就是回調

    當前不阻塞,事後來執行

  • 增加線程可能會引起資源競爭和併發問題

    通用問題

  • 並行的方式不是銀彈(不能解決所有問題)

來一個Spring-Event的 DEMO 的感受一下:

   public static void main(String[] args) {

        // 默認是同步非阻塞
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        // 構建線程池
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // 切換成異步非阻塞
        multicaster.setTaskExecutor(executor);

        // 增加事件監聽器
        multicaster.addApplicationListener(event -> { // Lambda 表達
            // 事件監聽
            System.out.printf("[線程 : %s] event : %s\n",
                    Thread.currentThread().getName(), // 當前執行線程名稱
                    event);
        });

        // 廣播事件
        multicaster.multicastEvent(new PayloadApplicationEvent("Hello,World", "Hello,World"));

        // 關閉線程池
        executor.shutdown();
    }

Reactor 認爲異步不一定能夠救贖

再次將以上觀點歸納,它認爲:

  • Callbacks 是解決非阻塞的方案,然而他們之間很難組合,並且快速地將代碼引導至 “Callback Hell” 的不歸路
  • Futures 相對於 Callbacks 好一點,不過還是無法組合,不過 CompletableFuture 能夠提升這方面的不足

串行與並行的效率測試

demo1、串行測試:

public class DataLoader {

    public final void load() {
        long startTime = System.currentTimeMillis(); // 開始時間
        doLoad(); // 具體執行
        long costTime = System.currentTimeMillis() - startTime; // 消耗時間
        System.out.println("load() 總耗時:" + costTime + " 毫秒");
    }

    protected void doLoad() { // 串行計算
        loadConfigurations();    //  耗時 1s
        loadUsers();                  //  耗時 2s
        loadOrders();                // 耗時 3s
    } // 總耗時 1s + 2s  + 3s  = 6s

    protected final void loadConfigurations() {
        loadMock("loadConfigurations()", 1);
    }

    protected final void loadUsers() {
        loadMock("loadUsers()", 2);
    }

    protected final void loadOrders() {
        loadMock("loadOrders()", 3);
    }

    private void loadMock(String source, int seconds) {
        try {
            long startTime = System.currentTimeMillis();
            long milliseconds = TimeUnit.SECONDS.toMillis(seconds);
            Thread.sleep(milliseconds);
            long costTime = System.currentTimeMillis() - startTime;
            System.out.printf("[線程 : %s] %s 耗時 :  %d 毫秒\n",
                    Thread.currentThread().getName(), source, costTime);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        new DataLoader().load();
    }

}

demo1運行結果:
在這裏插入圖片描述
demo2、並行測試:

public class ParallelDataLoader extends DataLoader {

    protected void doLoad() {  // 並行計算
        ExecutorService executorService = Executors.newFixedThreadPool(3); // 創建線程池
        CompletionService completionService = new ExecutorCompletionService(executorService);
        completionService.submit(super::loadConfigurations, null);      //  耗時 >= 1s
        completionService.submit(super::loadUsers, null);               //  耗時 >= 2s
        completionService.submit(super::loadOrders, null);              //  耗時 >= 3s

        int count = 0;
        while (count < 3) { // 等待三個任務完成
            if (completionService.poll() != null) {
                count++;
            }
        }
        executorService.shutdown();
    }  // 總耗時 max(1s, 2s, 3s)  >= 3s

    public static void main(String[] args) {
        new ParallelDataLoader().load();
    }

}

demo2運行結果:

在這裏插入圖片描述
以上測試流程圖:

load()loadConfigurations()loadUsers()loadOrders()load()loadConfigurations()loadUsers()loadOrders()

elastic與parallel性能對比測試:
elastic與parallel
關於串行與並行的理解:
在這裏插入圖片描述

並行+join:我們知道,在併發編程裏,join()方法可以等待線程銷燬,說白了,可以上多線程順序執行,常見的CountDownLatch則是通過AQS -> (狀態位、隊列 Integer)效果實現順序執行。

線程測試:

  public static void main(String[] args) throws InterruptedException {

        println("Hello,World 1");

        AtomicBoolean done = new AtomicBoolean(false);

        final  boolean isDone;

        // volatile 易變,線程安全(可見性)
        // final 不變,線程安全(一直不變)
        // final + volatile  = impossible

        Thread thread = new Thread(() -> {
            // 線程任務
            println("Hello,World 2020");
            // CAS
            done.set(true);  // 不通用
        });

        thread.setName("sub-thread");// 線程名字

        thread.start(); // 啓動線程

        // 線程 join() 方法
        thread.join(); // 等待線程銷燬

        println("Hello,World 2");
    }

    private static void println(String message) {
        System.out.printf("[線程 : %s] %s\n",
                Thread.currentThread().getName(), // 當前線程名稱
                message);
    }

運行結果:
在這裏插入圖片描述

關於Java 8 Lambda 表達式裏使用boolean類型變量問題:通過我們在一個事件裏使用標記位採用boolean,但是如果這個事件被lambda所封裝,就不能簡單的使用boolean,即使使用final修飾,所以在上面的demo裏採用AtomicBoolean(線程安全)作爲標記位。

至於說final volatile同時修飾爲什麼不行?原因很簡單,因爲兩個關鍵字本身就是對立的:

volatile 易變,線程安全(可見性)
final 不變,線程安全(一直不變)

CompletableFuture

Future 的侷限性

  • get() 方法是阻塞的
    在這裏插入圖片描述
  • Future 沒有辦法組合
    • 任務Future之間有依賴關係

    通俗講就是第一步的結果,是第二部的輸入

CompletableFuture 的功能列舉

  • 提供異步操作
  • 提供 Future 鏈式操作
  • 提供函數式編程

CompletableFuture測試

public class CompletableFutureDemo {

    public static void main(String[] args) {

        println("當前線程");

        // Reactive programming
        // Fluent 流暢的
        // Streams 流式的
        CompletableFuture.supplyAsync(() -> {
            println("第一步返回 \"Hello\"");
            return "Hello";
        }).thenApplyAsync(result -> { // 異步?
            println("第二步在第一步結果 +\",World\"");
            return result + ",World";
        }).thenAccept(CompletableFutureDemo::println) // 控制輸出
                .whenComplete((v, error) -> { // 返回值 void, 異常 -> 結束狀態
                    println("執行結束!");
                })
                .join() // 等待執行結束
        ;
    }

    private static void println(String message) {
        System.out.printf("[線程 : %s] %s\n",
                Thread.currentThread().getName(), // 當前線程名稱
                message);
    }
}

上述代碼採用的是命令編程方式(Imperative programming)
命令編程方式最大的特點是流程編排,其優勢在於:

  • 大多數業務邏輯是數據操作
  • 消費類型 Consumer
  • 轉換類型 Function
  • 提升/減少維度 map/reduce/flatMap

傳統的編程模式:三段式編程

        try {
            // 1、業務執行
            // action
        } catch (Exception e) {
         // 2、異常處理
            // error
        } finally {
         // 3、執行完成
            //  complete
        }

CompletableFutureDemo 運行結果爲:
在這裏插入圖片描述
以上流程圖爲:

main()supplyAsync()thenApplyAsync()thenAccept()異步操作main()supplyAsync()thenApplyAsync()thenAccept()

函數式編程 + Reactive

  • Reactive programming
  • 編程風格
    • Fluent 流暢的
    • Streams 流式的
  • 業務效果
    • 流程編排
    • 大多數業務邏輯是數據操作
  • 函數式語言特性(Java 8+)
    • 消費類型 Consumer
    • 生產類型 Supplier
    • 轉換類型 Function
    • 判斷類型 Predicate
    • 提升/減少維度 map/reduce/flatMap

來一個steam流式處理操作demo:

        Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) // 0-9 集合
                .filter(v -> v % 2 == 1) // 判斷數值->獲取奇數
                .map(v -> v - 1) // 奇數變偶數
                .reduce(Integer::sum) // 聚合操作
                .ifPresent(System.out::println) // 輸出 0 + 2 + 4 + 6 + 8

以上操作是不是非常直觀? 就是一次性的將數據處理完成!

其實不論Java/C#/JS/Python/Scale/Koltin語言,都在使用這種操作(Reactive/Stream模式)

Stream 是迭代器( Iterator )模式,數據已完全準備,拉模式(Pull)
Reactive 是觀察者(Observer)模式,來一個算一個,推模式(Push),當有數據變化的時候,作出反應(Reactor)

Reactive Programming

Reactive Programming 作爲觀察者模式的延伸,不同於傳統的命令編程方式同步拉取數據的方式,如迭代器模式。而是採用數據發佈者同步或異步地推送到數據流(Data Streams)的方案。當該數據流(Data Streams)定於這堅挺到傳播變化時,立即做出響應動作。在實現層面上,Reactive Programming 可結合函數式編程簡化面嚮對象語言語法的臃腫性,屏蔽併發實現的複雜細節,提供數據流的有序操作,從而達到提升代碼的可讀性,以及減少Bugs出現的目的。同時,Reactive Programming結合背壓(Backpressure)的技術解決發佈端生成數據的速率高於訂閱端消費的問題。

WebFlux 使用場景

先上一個demo:

  public static void main(String[] args) throws InterruptedException {

        Flux.just(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) // 直接執行
                .filter(v -> v % 2 == 1) // 判斷數值->獲取奇數
                .map(v -> v - 1) // 奇數變偶數
                .reduce(Integer::sum) // 聚合操作
                .subscribeOn(Schedulers.elastic())
//                .subscribeOn(Schedulers.parallel())
//                .block());
                .subscribe(ReactorDemo::println) // 訂閱才執行
        ;
        Thread.sleep(1000);
    }

    private static void println(Object message) {
        System.out.printf("[線程 : %s] %s\n",
                Thread.currentThread().getName(), // 當前線程名稱
                message);
    }

執行結果:
在這裏插入圖片描述
我們發現WebFlux 的特性:

  • 長期異步執行,一旦提交,慢慢操作。

那麼是否適合 RPC 操作?

  • 任務型的,少量線程,多個任務長時間運作,達到伸縮性。

Flux 和 Mono 是 Reactor 中的兩個基本概念。
Mono:單數據 Optional 0:1, RxJava : Single
Flux : 多數據集合,Collection 0:N , RxJava : Observable

同樣,再舉一個栗子,看看WebFlux 與SpringMVC的性能比較:

@RestController
public class WebFluxController {

    @RequestMapping("")
    public Mono<String> index() {
        // 執行計算
        println("執行計算");
        Mono<String> result =  Mono.fromSupplier(() -> {

            println("返回結果");

            return "Hello,World";
        });

        return result;
    }

    private static void println(String message) {
        System.out.printf("[線程 : %s] %s\n",
                Thread.currentThread().getName(), // 當前線程名稱
                message);
    }
}

測試結果(這裏使用WebFluxApplication作爲啓動類,並在瀏覽器訪問http://localhost:8080/):
在這裏插入圖片描述

相對SpringMVC來說,WebFlux執行效率不見得比SpringMVC要快,只是WebFlux在多線程異步處理方面比較友好,使得其具有更好伸縮性,其底層還是基於併發編程。

使用場景

  1. 函數式編程

  2. 非阻塞(同步/異步)

  3. 遠離 Servlet API
    Servlet
    HttpServletRequest

  4. 不再強烈依賴 Servlet 容器(兼容)
    Tomcat
    Jetty

實際上很多技術基於Reactor做了實現,Reactor真的可以引領下一代麼?
Spring Cloud Gateway -> Reactor

Spring WebFlux -> Reactor

Zuul2 -> Netty Reactive

WebFlux 整體架構

在這裏插入圖片描述

後記

本節示例代碼:https://github.com/harrypottry/microservices-project/tree/master/spring-reactive

更多架構知識,歡迎關注本套Java系列文章Java架構師成長之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章