引子:被譽爲「中國大數據第一人」的塗子沛先生在其成名做《數據之巔》裏提到,摩爾定律、社交媒體、數據挖掘是大數據的三大成因。IBM的研究稱,整我的類文明所得到的所有數據中,有90%是過去兩年內產生的。在此背景下,包括NoSQL,Hadoop, Spark, Storm, Kylin在內的大批新技術應運而生。其中以RxJava和Reactor爲表明的響應式(Reactive)編程技術針對的就是經典的大數據4V定義(Volume,Variety,Velocity,Value)中的Velocity,即高併發問題,而在即將發佈的Spring 5中,也引入了響應式編程的支持。在接下來的幾周,我會圍繞響應式編程分三期與你分享個人一些學習心得。本篇是第二篇,以Reactor框架爲例介紹響應式編程的幾個關鍵特性。java
前情概要:react
In computing, reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. - Reactive programming - Wikipediagit
在上述響應式編程(後面簡稱RP)的定義中,除了異步編程,還包含兩個重要的關鍵詞:github
一個容易混淆的概念是響應式設計,雖然它的名字中也包含了「響應式」三個字,但其實和RP徹底是兩碼事。響應式設計是指網頁可以自動調整佈局和樣式以適配不一樣尺寸的屏幕,屬於網站設計的範疇,而RP是一種關注系統可響應性,面向數據流的編程思想或者說編程框架。web
從本質上說,RP是一種異步編程框架,和其餘框架相比,RP至少包含了如下三個特性:spring
subscribe()
方法以前,從發佈端到訂閱端,沒有任何事會發生。就比如不管多長的水管,只要水龍頭不打開,水管裏的水就不會流動。爲了提升描述能力,RP提供了比Stream豐富的多的多的API,好比buffer()
, merge()
, onErrorMap()
等。瞭解了RP的這些特性,你可能已經猜測到RP有哪些適用場景了。通常來講,RP適用於高併發、帶延遲操做的場景,好比如下這些狀況(的組合):編程
Every coin has two sides.api
和任何框架同樣,有優點必然就有劣勢。RP的兩個比較大的問題是:數組
flux.map(String::toUpperCase).doOnNext(s -> LOG.info("UC String {}", s)).next().subscribe()
,一旦出錯,你將很難定位到具體是哪一個環節出了問題。所幸的是,RP框架通常都會提供一些工具方法來輔助進行調試。爲了幫助你理解上面說的一些概念,下面我就經過幾個測試用例,演示RP的兩個關鍵特性:提升吞吐量和背壓。完整的代碼可參見我GitHub上的示例工程。緩存
@Test
public void testImperative() throws InterruptedException {
_runInParallel(CONCURRENT_SIZE, () -> {
ImperativeRestaurantRepository.INSTANCE.insert(load);
});
}
private void _runInParallel(int nThreads, Runnable task) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
for (int i = 0; i < nThreads; i++) {
executorService.submit(task);
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
@Test
public void testReactive() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(CONCURRENT_SIZE);
for (int i = 0; i < CONCURRENT_SIZE; i++) {
ReactiveRestaurantRepository.INSTANCE.insert(load).subscribe(s -> {
}, e -> latch.countDown(), latch::countDown);
}
latch.await();
}複製代碼
用例解讀:
在演示測試用例以前,先看兩張圖,幫助你更形象的理解什麼是背壓。
圖片出處:Dataflow and simplified reactive programming
兩張圖乍一看沒啥區別,但實際上是徹底兩種不一樣的背壓策略。第一張圖,發佈速度(100/s)遠大於訂閱速度(1/s),但因爲背壓的關係,發佈者嚴格按照訂閱者的請求數量發送數據。第二張圖,發佈速度(1/s)小於訂閱速度(100/s),當訂閱者請求100個數據時,發佈者會積滿所需個數的數據再開始發送。能夠看到,經過背壓機制,發佈者能夠根據各個訂閱者的能力動態調整發布速度。
@BeforeEach
public void beforeEach() {
// initialize publisher
AtomicInteger count = new AtomicInteger();
timerPublisher = Flux.create(s ->
new Timer().schedule(new TimerTask() {
@Override
public void run() {
s.next(count.getAndIncrement());
if (count.get() == 10) {
s.complete();
}
}
}, 100, 100)
);
}
@Test
public void testNormal() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
timerPublisher
.subscribe(r -> System.out.println("Continuous consuming " + r),
e -> latch.countDown(),
latch::countDown);
latch.await();
}
@Test
public void testBackpressure() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Subscription> timerSubscription = new AtomicReference<>();
Subscriber<Integer> subscriber = new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
timerSubscription.set(subscription);
}
@Override
protected void hookOnNext(Integer value) {
System.out.println("consuming " + value);
}
@Override
protected void hookOnComplete() {
latch.countDown();
}
@Override
protected void hookOnError(Throwable throwable) {
latch.countDown();
}
};
timerPublisher.onBackpressureDrop().subscribe(subscriber);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
timerSubscription.get().request(1);
}
}, 100, 200);
latch.await();
}複製代碼
用例解讀:
經過上面的介紹,不難看出RP其實是一種內置了發佈者訂閱者模型的異步編程框架,包含了線程複用,背壓等高級特性,特別適用於高併發、有延遲的場景。
以上就是我對響應式編程的一些簡單介紹,歡迎你到個人留言板分享,和你們一塊兒過過招。下一篇我將綜合前兩篇的內容,詳解一個完整的Spring 5示例應用,敬請期待。