借助JDK8和RxJava如何讓你的業務代碼運行的更快

背景

微服務流行后,在我們項目開發過程中,一個服務經常會調用N個微服務,調用每個微服務可能需要幾百毫秒,試想,一個復雜的業務如果要調用上百的微服務,如果各個服務同步執行,可能就需要花費好幾秒,試想:這些服務為什么不能并行運行呢?

一個復雜的計算任務,為什么不能分解成更小的任務單位,讓他們并行運行呢?

本文通過以上兩個業務場景,比較各個實現方案的差異,在講解之前,我們先來了解下本文提到的RxJava

案例

從一段最簡單的服務開始:該服務需調用3個微服務,每個微服務費時250ms,三個微服務都獲取數據后返回給前端(該微服務三個服務分別是商品詳情,商品評論和推薦商品列表),如果按順序執行,那么代碼是這樣的:

public static void main(String[] args) throws Exception {
    long c = System.currentTimeMillis();
    System.out.println("順序執行:");
    System.out.println(service("商品詳情微服務")+service("商品評論微服務")+service("推薦商品微服務"));
    spendTime(c);
}
//模擬某個服務
private static String service(String srvName){
    try {
        Thread.sleep(250);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return srvName+"\r\n";
}
private static void spendTime(long preTime) {
    System.out.println("花費:" + (System.currentTimeMillis() - preTime) + " 毫秒");
}

這段代碼毫無疑問,打印輸出:

花費:781 毫秒

改造一下,使用JDK8的CompletableFuture,3個微服務獨立線程運行,都完成后通知主線程打印,代碼如下:

public static void main(String[] args) throws Exception {
        final long cc = System.currentTimeMillis(); 
    CompletableFuture<String> s1 = CompletableFuture.supplyAsync(() -> service("商品詳情微服務"));
    CompletableFuture<String> s2 = CompletableFuture.supplyAsync(() -> service("商品評論微服務"));
    CompletableFuture<String> s3 = CompletableFuture.supplyAsync(() -> service("推薦商品微服務"));
    s1.thenCombine(s2, (i,j)->{
        return i+j;
    }).thenCombine(s3, (i,j)->{
        System.out.println("使用JDK8的并行編程:");
        System.out.println(i+j);
        spendTime(cc);
        return i+j;
    });
}

以上代碼的執行結果取決于3個微服務中最長時間的那個服務,相比原先速度有明顯提高:

花費:311 毫秒

那么以上的代碼使用RxJava怎么來寫呢?我們可以flatMap將服務分拆到各自獨立線程中去執行,代碼如下:

private static String[] ss = {"商品詳情微服務","商品評論微服務","推薦商品微服務"};
public static void main(String[] args) throws Exception {
    Observable.range(0,3)
    .flatMap(new Function<Integer, ObservableSource<String>>() {
        @Override
        public ObservableSource<String> apply(Integer t) throws Exception {
            return Observable.just(t)
        .subscribeOn(Schedulers.from(Executors.newFixedThreadPool(1)))
                .map(new Function<Integer, String>() {
                    @Override
                    public String apply(Integer t) throws Exception {
                        return service(ss[t]);
                    }
                });
            }
        })
        .reduce((s1,s2)->s1+s2)
        .subscribe(s -> {
            System.out.println("Observable:\r\n" + s);
            spendTime(cc2);
        });
}

花費:455 毫秒

RxJava模擬的針對每個數據項的并發操作調用時間上要比直接使用JDK8的API慢得多

第二個業務場景是將復雜的計算進行拆分子計算任務,然后將每個任務計算合并成最終計算結果,以下直接給出所有源碼,我們來看看幾種計算方式在耗時上的不同,復雜計算任務是:對1到210000000開根號求總和

package com.sumslack.rxjava;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.functions.BiFunction;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;

public class TestComputer {
    private static final int MAX_I = 210000000;
    
    private static void spendTime(long preTime) {
        System.out.println("花費:" + (System.currentTimeMillis() - preTime) + " 毫秒");
    }
    
    private static void spendTime(long preTime,String str) {
        System.out.println("[" + str + "] 花費:" + (System.currentTimeMillis() - preTime) + " 毫秒");
    }
    private static ExecutorService eService = Executors.newCachedThreadPool();
    public static void main(String[] args) throws Exception{
        
        int[] ss = new int[MAX_I];
        for(int i=1;i<=MAX_I;i++) {
            ss[i-1] = i;
        }
        
        
        long c = System.currentTimeMillis();
        System.out.println(xx(0,MAX_I));
        spendTime(c,"順序執行");

        final long cc5 = System.currentTimeMillis();
        Observable.range(1, MAX_I).map(new Function<Integer, Double>() {
            @Override
            public Double apply(Integer t) throws Exception {
                return Math.sqrt(t);
            }
        }).reduce((i,j)->i+j)
        .subscribeOn(Schedulers.computation())
        .subscribe(s -> {
            spendTime(cc5,"Observable直接算");
        });
        final long cc = System.currentTimeMillis();
        CompletableFuture<Double> cf1 = CompletableFuture.supplyAsync(() -> {
            return xx(0,MAX_I/2);
        });
        CompletableFuture<Double> cf2 = CompletableFuture.supplyAsync(() -> {
            return xx(MAX_I/2,MAX_I);
        });
        cf1.thenCombine(cf2,  (i,j)->{
            System.out.println(""+(i+j));
            spendTime(cc,"CompletableFuture");
            return i+j;
        });
               
        //也可以用:CompletableFuture.allOf(cf1,cf2).join();
        c = System.currentTimeMillis();
        Double dd = Arrays.stream(ss).mapToDouble(d -> Math.sqrt(d)).reduce(0d,Double::sum);
        System.out.println(dd);
        spendTime(cc,"stream");
        
        c = System.currentTimeMillis();
        Double dd2 = Arrays.stream(ss).parallel().mapToDouble(d -> Math.sqrt(d)).reduce(0d,Double::sum);
        System.out.println(dd2);
        spendTime(cc,"parallel stream");
        
        final long cc2 = System.currentTimeMillis();
        Observable.fromArray(0,1,2)
        .flatMap(new io.reactivex.functions.Function<Integer,ObservableSource<Double>>(){
            @Override
            public ObservableSource<Double> apply(Integer t) throws Exception {
                if(t%3==0) {
                    return Observable.just(t)
                        .subscribeOn(Schedulers.computation())
                        .map(new Function<Integer, Double>() {
                            @Override
                            public Double apply(Integer t) throws Exception {
                                return xx(0,MAX_I/3);
                            }
                        });
                }else if(t%3==1) {
                    return Observable.just(t)
                            .subscribeOn(Schedulers.computation())
                            .map(new Function<Integer, Double>() {
                                @Override
                                public Double apply(Integer t) throws Exception {
                                    return xx(MAX_I/3,MAX_I*2/3);
                                }
                            });
                }else {
                    return Observable.just(t)
                            .subscribeOn(Schedulers.computation())
                            .map(new Function<Integer, Double>() {
                                @Override
                                public Double apply(Integer t) throws Exception {
                                    return xx(MAX_I*2/3,MAX_I);
                                }
                            });
                }
            }
        })
        .reduce(new BiFunction<Double, Double, Double>() {
            @Override
            public Double apply(Double t1, Double t2) throws Exception {
                return t1+t2;
            }
        })
        .subscribe( s->{
            System.out.println(s);
            spendTime(cc2,"Observable");
        });
        Thread.sleep(100000);
    }
    
    private static double xx(int start,int end) {
        double sum = 1;
        for(int i=start;i<end;i++) {
            sum += Math.sqrt(i+1);
        }
        return sum;
    }
}

以下是費時結果:

[順序執行] 花費:1086 毫秒
[CompletableFuture] 花費:537 毫秒
[stream] 花費:1028 毫秒
[parallel stream] 花費:1305 毫秒
[Observable] 花費:461 毫秒
[Observable直接算] 花費:4265 毫秒

這里使用 RxJava 進行計算任務分解求和是最快的,因為JDK8并發編程我們分解的是兩個計算任務,而RxJava分解成3個所致!

關于RxJava

RxJavaReactive ExtensionsJava實現,通過使用Obserable/Flowable序列來構建異步和基于事件的程序的庫,RxJava實現和擴展了觀察者模式。

RxJava基于響應式編程,是一種面向數據流和變化傳播的編程范式。傳統編程方式代碼都是順序執行的,而響應式編程是基于異步編程的,借助于CPU多核能力,提高運行效率,降低延遲和阻塞,基于數據流模型,如一個函數可作用與數據流中的每項,可變化傳播。在響應式編程中,函數成為其第一等公民,同原型類型一樣,函數可作用與參數,也可作為返回值。

RxJava基于函數式編程,傳統面向對象是通過抽象出對象關系來解決問題,函數式編程是通過函數的組合來解決問題。

概念

  • Observable:被訂閱者,比如在安卓開發中,可能是某個數據源,數據源的變化要通知到UI,那么UI就是Observer,被訂閱者有冷熱之分,熱Observable無論有沒有訂閱者訂閱,事件流始終發送,而冷Observable則只有訂閱者訂閱事件流才開始發送數據,它們之間是可以通過API相互轉化的,比如使用publish可以冷->熱,RefCount可以熱->冷;
  • Observer:訂閱者;

RxJava編程

  • 被訂閱者:用的做多的是Observable,如果要支持背壓則使用Flowable,還可以使用Single(只要OnSuccess和onError,沒有onComplete),Completable(創建后不發射任何數據,只有onComplete和onError)和Maybe(只發送0或1個數據);
  • 生命周期監聽:Observable創建后可使用doXXX監聽你說需要的生命周期回調;
  • 流的創建:create(使用一個函數從頭創建),just(指定值創建,最多10個),fromXXX(基于X類創建),repeat(特定數據重復N次創建),defer(直到有訂閱者訂閱時才創建),interval(每隔一段時間創建一個數據發送),timer(延遲一段時間后發送數據);
  • RxJava線程模型: 內置多個線程控制器,包括single(定長為1的線程池),newThread(啟動新線程執行),computation(大小為CPU核數線程池,一般用于密集型計算),io(適用IO操作),trampoline(直接在當前線程運行)和Schedulers.from(自定義);
  • 變化操作符:map(數據轉型),flatMap(數據轉某個Observable后合并發送),scan(每個數據應用一個函數,然后按順序發送),groupBy(按Key分組拆分成多個Observable),buffer(打包發送),window,cast(強制轉換類型);
  • 過濾操作:filter(按條件過濾),takeLast(只發送最后N個數據),last(只發送最后一個數據),lastOrDefault(只發送最后一個數據,為Null發送默認值),takeLastBuffer(將最后N個數據當做單個數據發送),skip(跳過N個發送),skipLast(跳過最后N個),take(只發送開始的N個數據),first,takeFirst(只發送滿足條件的第一個數據),elementAt(只發送第N個數據),timeout(指定事件內沒發送數據,就發送異常),distinct(去重),ofType(只發送特定類型的數據),ignoreElements(丟失所有正常數據,只發送錯誤或完成通知),sample(一段時間內,只處理最后一個數據),throttleFirst(一段時間內,只處理第一個數據),debounce(發送一個數據,開始計時,到了規定時間沒有再發送數據,則開始處理數據);
  • 條件操作和布爾操作符:all(發送的數據是否都滿足條件),contains(發送的數據是否包含某數據),amb(多個被訂閱者數據發送只發送首次被訂閱的那個數據流),defaultIfEmpty(如果原始被訂閱者沒有值,則發送一個默認值),sequenceEquals(判定兩個數據流是否一樣,返回true或false),skipUtil(直到符合條件才發送),skipWhile(直到條件不符合才開始發送),takeUntil(滿足條件后不發送)和takeWhile(條件滿足的一直發送);
  • 合并和連接操作符:merge(將多個被訂閱數據流合并),zip(將多個數據流結合發送,返回數據流的數據個數是最少的那個),combineLastest(類似zip,任意被訂閱者開始發送數據時即發送,而zip要每個被訂閱者開始發送數據才發送),join(兩個被訂閱者結合合并,總數據項是M*N項),startWith(在數據序列開頭插入指定項),connect,靈活控制發送數據規則可使用push,refCount,replay(保證所有訂閱者收到相同數據);
  • 背壓:被訂閱者發送數據過快以至于訂閱者來不及處理的情況;

總結

對于復雜計算,你可以將計算任務分解成N個子計算任務,交給多個線程處理并將結果合并后取得最終結果,對于服務業務的調用,你應該清楚,哪些子任務可以并行運行,哪些需要順序執行,使用RxJava在代碼上可能更加直觀,也可以使用JDK8的CompletableFuture,其實JDK8的很多API參考了RxJava的實現,兩者在寫法上非常的類似,響應式編程相比傳統代碼的順序執行在思路上有很大的不同,理解上也有一定的難度,希望通過本文讓您全面了解函數式編程的實現思路。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容