目錄導航
前言
前面的章節我們講了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 觀點歸納如下,它認爲:
- 阻塞導致性能瓶頸和浪費資源
- 任何代碼都是阻塞(指令是串行)
- 非阻塞從實現來說,就是回調
當前不阻塞,事後來執行
- 增加線程可能會引起資源競爭和併發問題
通用問題
- 並行的方式不是銀彈(不能解決所有問題)
來一個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運行結果:
以上測試流程圖:
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 運行結果爲:
以上流程圖爲:
函數式編程 + 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在多線程異步處理方面比較友好,使得其具有更好伸縮性,其底層還是基於併發編程。
使用場景
-
函數式編程
-
非阻塞(同步/異步)
-
遠離 Servlet API
Servlet
HttpServletRequest
-
不再強烈依賴 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架構師成長之路