深入講解RxJava響應式編程框架,背壓問題的幾種應對模式 背壓 什麼是背壓問題 背壓問題的幾種應對模式

文章首發公衆號:Java架構師聯盟,每日更新技術好文

背壓

本節首先介紹什麼是背壓(Backpressure)問題,然後介紹背壓問題的幾種應對模式。

什麼是背壓問題

當上下游的流操作處於不同的線程時,如果上游彈射數據的速度快於下游接收處理數據的速度,對於那些沒來得及處理的數據就會造成積壓,這些數據既不會丟失,又不會被垃圾回收機制回收,而是存放在一個異步緩存池中,如果緩存池中的數據一直得不到處理,越積越多,最後就會造成內存溢出,這便是響應式編程中的背壓問題。

一個存在背壓問題的演示實例代碼如下:

package com.crazymaker.demo.rxJava.basic;
//省略import
@Slf4j
public class BackpressureDemo {
/**
*演示不使用背壓
*/
@Test
public void testNoBackpressure() throws InterruptedException {
//被觀察者(主題)
Observable observable = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
//循環10次
for (int i = 0;i<10 ; i++) {
log.info("produce ->" + i);
subscriber.onNext(String.valueOf(i));
}
}
});
//觀察者
Action1<String> subscriber = new Action1<String>() {
public void call(String s){
try {
//每消費一次間隔50毫秒
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("consumer ->" + s);
}
};
//訂閱:observable與subscriber之間依然通過subscribe()進行關聯
observable
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.subscribe(subscriber);
Thread.sleep(Integer.MAX_VALUE);
}
}</pre>

在實例代碼中,observable發射操作執行在一條通過Schedulers.io()調度器獲取的IO線程上,而觀察者subscriber的消費操作執行在另一條通過Schedulers.newThread()調度器獲取的新線程上。observable流不斷髮送數據,累積發送10次;觀察者subscriber每隔50毫秒接收一條數據。

運行上面的演示程序後,輸出的結果如下:

17:56:17.719 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->0
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->1
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->2
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->3
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->4
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->5
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->6
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->7
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->8
17:56:17.723 [RxIoScheduler-2] INFO c.c.d.r.b.BackpressureDemo - produce ->9
17:56:17.774 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->0
17:56:17.824 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->1
17:56:17.875 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->2
17:56:17.925 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->3
17:56:17.976 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->4
17:56:18.027 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->5
17:56:18.078 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->6
17:56:18.129 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->7
17:56:18.179 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->8
17:56:18.230 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->9</pre>

上面的程序有一個特點:生產者observable彈射數據的速度大於下游消費者subscriber接收處理數據的速度,但是由於數據量小,因此上面的程序運行起來沒有出現問題。

簡單修改一下生產者,將原來的彈射10條改成無限制地彈射,代碼如下:

//被觀察者(主題)
Observable observable = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
//無限制地循環
for (int i = 0; ; i++) {
//log.info("produce ->" + i); subscriber.onNext(String.valueOf(i));
}
}
});</pre>

再次運行該演示程序後,拋出的異常如下:

Caused by: rx.exceptions.MissingBackpressureException
at rx.internal.operators.OperatorObserveOnObserveOnSubscriber.onNext (OperatorObserveOn.java:160) at rx.internal.operators.OperatorSubscribeOnSubscribeOnSubscriber.onNext
(OperatorSubscribeOn.java:74)
at com.crazymaker.demo.rxJava.basic.BackpressureDemo1.call (BackpressureDemo.java:24) at com.crazymaker.demo.rxJava.basic.BackpressureDemo1.call
(BackpressureDemo.java:19)
at rx.Observable.unsafeSubscribe(Observable.java:10327)
at rx.internal.operators.OperatorSubscribeOnSubscribeOnSubscriber.call (OperatorSubscribeOn.java:100) at rx.internal.schedulers.CachedThreadSchedulerEventLoopWorker$1.call
(CachedThreadScheduler.java:230)
... 9 more</pre>

異常原因:由於上游observable流彈射數據的速度遠遠大於下游通過subscriber接收的速度,導致observable用於暫存彈射數據的隊列空間耗盡,造成上游數據積壓。

背壓問題的幾種應對模式

如何應對背壓問題呢?在創建主題時可以使用Observable類的一個重載的create方法設置具體的背壓模式,該方法的源代碼如下:

public static <T> Observable<T> create(Action1<Emitter<T>> emitter, Emitter.BackpressureMode backpressure) {
return unsafeCreate(new OnSubscribeCreate<T>(emitter, backpressure));
}</pre>

此方法的第二個參數用於指定一種背壓模式。背壓模式有多種,比較常用的有“最近模式” Emitter.BackpressureMode.LATEST。這種模式的含義爲:如果消費跟不上,那麼僅僅緩存最近彈射出來的數據,將老舊一點的數據直接丟棄。

使用“最近模式”背壓,改寫4.8.1節的測試用例,代碼如下:

/**
*演示使用“最近模式”背壓
*/
@Test
public void testBackpressure() throws InterruptedException {
//主題實例,使用背壓
Observable observable = Observable.create(
new Action1<Emitter<String>> () {
@Override
public void call(Emitter<String> emitter) {
//無限循環
for (int i = 0; ; i++) {
//log.info("produce ->" + i);
emitter.onNext(String.valueOf(i));
}
}
}, Emitter.BackpressureMode.LATEST);
//訂閱者(觀察者)
Action1<String> subscriber = new Action1<String>() {
public void call(String s) {
try {
//每消費一次間隔50毫秒
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("consumer ->" + s);
}
};
//訂閱: observable與subscriber之間依然通過subscribe()進行關聯
observable
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.subscribe(subscriber);
Thread.sleep(Integer.MAX_VALUE);
}</pre>

運行這個演示程序,部分輸出的結果節選如下:

18:51:54.736 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->0
18:51:54.745 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->1
//省略部分輸出
18:51:55.217 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->123
18:51:55.220 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->124
18:51:55.224 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->125
18:51:55.228 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->126
18:51:55.232 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->127
18:51:55.236 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->7337652
18:51:55.240 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->7337653
18:51:55.244 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->7337654
//省略部分輸出
18:51:55.595 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->7337747
18:51:55.598 [RxNewThreadScheduler-1] INFO c.c.d.r.b.BackpressureDemo - consumer ->14161628</pre>

從輸出的結果可以看到,上游主題連續不斷地彈射,下游訂閱者在接收完127後直接跳到了7337652,其間彈射出來的幾百萬數據(相對舊一點的數據)就直接被丟棄了。

除了 Emitter.BackpressureMode.LATEST“最近模式”外,RxJava在Emitter<T>接口中通過一個枚舉常量定義了以下幾種背壓模式:

enum BackpressureMode {
/**
*No backpressure is applied(無背壓模式)
*可能導致rx.exceptions.MissingBackpressureException異常
或者IllegalStateException異常
/
NONE,
/

如果消費者跟不上,就拋出rx.exceptions.MissingBackpressureException異常
/
ERROR,
/

緩存所有的onNext方法彈射出來的消息,等待消費者慢慢地消費
/
BUFFER,
/

如果下游消費跟不上,就丟棄onNext方法彈射出來的新消息
/
DROP,
/

*如果消費者跟不上,就丟掉舊的消息,緩存onNext方法彈射出來的新消息
*/
LATEST
}</pre>

對於以上RxJava背壓模式,介紹如下:

(1)BackpressureMode.DROP:在這種模式下,Observable主題使用固定大小爲128的緩衝區。如果下游訂閱者無法處理,流的第一個元素就會緩存下來,後續的會被丟棄。

(2)BackpressureMode.LATEST:這種模式與BackpressureMode.DROP類似,並且Observable主題也使用固定大小爲128的緩衝區。BackpressureMode.LATEST的緩存策略不同,使用最新的彈出元素替換緩衝區緩存的元素。當消費者可以處理下一個元素時,它收到的是Observable最近一次彈出的元素。

(3)BackpressureMode.NONE和BackpressureMode.ERROR:在這兩種模式中發送的數據不使用背壓。如果上游observable主題彈射數據的速度大於下游通過subscriber接收的速度,造成上游數據積壓,就會拋出 MissingBackpressureException異常。

(4)BackpressureMode.BUFFER:在這種模式下,有一個無限的緩衝區(初始化時是128),下游消費不了的元素全部會放到緩衝區中。如果緩衝區中持續地積累,就會導致內存耗盡,拋出OutOfMemoryException異常。
文章首發公衆號:Java架構師聯盟,每日更新技術好文

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