02.28 實踐:使用了CompletableFuture之後,程序性能提升了三倍

CompletableFuture

相比於jdk5所提出的future概念,future在執行的時候支持異步處理,但是在回調的過程中依舊是難免會遇到需要等待的情況。

在jdk8裡面,出現了CompletableFuture的新概念,支持對於異步處理完成任務之後自行處理數據。當發生異常的時候也能按照自定義的邏輯來處理。

如何通過使用CompletableFuture提升查詢的性能呢?

下邊我舉個例子來演示:

首先我們定義一個UserInfo對象:

<code>/** * @author idea * @data 2020/2/22 */public class UserInfo {    private Integer id;    private String name;    private Integer jobId;    private String jobDes;    private Integer carId;    private String carDes;    private Integer homeId;    private String homeDes;    public Integer getId() {        return id;    }    public UserInfo setId(Integer id) {        this.id = id;        return this;    }    public String getName() {        return name;    }    public UserInfo setName(String name) {        this.name = name;        return this;    }    public Integer getJobId() {        return jobId;    }    public UserInfo setJobId(Integer jobId) {        this.jobId = jobId;        return this;    }    public String getJobDes() {        return jobDes;    }    public UserInfo setJobDes(String jobDes) {        this.jobDes = jobDes;        return this;    }    public Integer getCarId() {        return carId;    }    public UserInfo setCarId(Integer carId) {        this.carId = carId;        return this;    }    public String getCarDes() {        return carDes;    }    public UserInfo setCarDes(String carDes) {        this.carDes = carDes;        return this;    }    public Integer getHomeId() {        return homeId;    }    public UserInfo setHomeId(Integer homeId) {        this.homeId = homeId;        return this;    }    public String getHomeDes() {        return homeDes;    }    public UserInfo setHomeDes(String homeDes) {        this.homeDes = homeDes;        return this;    }}/<code>

這個對象裡面的homeid,jobid,carid都是用於匹配對應的住房信息描述,職業信息描述,購車信息描述。

對於將id轉換為描述信息的方式需要通過額外的sql查詢,這裡做了個簡單的工具類來進行模擬:

<code>import java.util.concurrent.TimeUnit;import java.util.function.Supplier;/** * @author idea * @data 2020/2/22 */public class QueryUtils {    public String queryCar(Integer carId){        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        return "car_desc";    }    public String queryJob(Integer jobId){        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        return "job_desc";    }    public String queryHome(Integer homeId){        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        return "home_desc";    }}/<code>

這個工具類的功能看起來會比較通俗易懂,在常規的邏輯裡面,我們做批量對象的轉換大多數都是基於List遍歷,然後在循環裡面批量查詢,這樣的方式並非說不行,而是顯得比較過於“暴力”。

假設每次查詢需要消耗1s,那麼遍歷的一個size為n的集合查詢消耗的時間就是n * 3s。

下邊來介紹一種更為方便的技巧:CompletableFuture

定義一個QuerySupplier 實現Supplier接口,根據注入的類型進行轉譯查詢:

<code>import java.util.function.Supplier;public class QuerySuppiler implements Supplier<string> {        private Integer id;        private String type;        private QueryUtils queryUtils;        public QuerySuppiler(Integer id, String type,QueryUtils queryUtils) {            this.id = id;            this.type = type;            this.queryUtils=queryUtils;        }        @Override        public String get() {            if("home".equals(type)){                return queryUtils.queryHome(id);            }else if ("job".equals(type)){                return queryUtils.queryJob(id);            }else if ("car".equals(type)){                return queryUtils.queryCar(id);            }            return null;        }    }/<string>/<code>

由於對應的carid,homeid,jobid都需要到指定的k,v配置表裡面通過核心查詢包裝器來進行轉譯,因此通常的做法就是在for循環裡面一個個地進行遍歷解析,這樣的做法也比較易於理解。

QuerySuppiler 是我寫的一個用於做對象解析的服務,代碼如下所示:

<code>import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.function.Consumer;import java.util.function.Supplier;import java.util.stream.Collectors;/** * @author idea * @data 2020/2/22 */public class QueryUserService {    private Supplier<queryutils> queryUtilsSupplier = QueryUtils::new;    public UserInfo converUserInfo(UserInfo userInfo) {        QuerySuppiler querySuppiler1 = new QuerySuppiler(userInfo.getCarId(), "car", queryUtilsSupplier.get());        CompletableFuture<string> getCarDesc = CompletableFuture.supplyAsync(querySuppiler1);        getCarDesc.thenAccept(new Consumer<string>() {  --1            @Override            public void accept(String carDesc) {                userInfo.setCarDes(carDesc);            }        });        QuerySuppiler querySuppiler2 = new QuerySuppiler(userInfo.getHomeId(), "home", queryUtilsSupplier.get());        CompletableFuture<string> getHomeDesc = CompletableFuture.supplyAsync(querySuppiler2);        getHomeDesc.thenAccept(new Consumer<string>() {  --2            @Override            public void accept(String homeDesc) {                userInfo.setHomeDes(homeDesc);            }        });        QuerySuppiler querySuppiler3 = new QuerySuppiler(userInfo.getJobId(), "job", queryUtilsSupplier.get());        CompletableFuture<string> getJobDesc = CompletableFuture.supplyAsync(querySuppiler3);        getJobDesc.thenAccept(new Consumer<string>() {  --3            @Override            public void accept(String jobDesc) {                userInfo.setJobDes(jobDesc);            }        });        CompletableFuture<void> getUserInfo = CompletableFuture.allOf(getCarDesc, getHomeDesc, getJobDesc);        getUserInfo.thenAccept(new Consumer<void>() {            @Override            public void accept(Void result) {                System.out.println("全部完成查詢" );            }        });        getUserInfo.join();  --4        return userInfo;    }    public static void main(String[] args) {        long begin= System.currentTimeMillis();        //多線程環境需要注意線程安全問題        List<userinfo> userInfoList=Collections.synchronizedList(new ArrayList<>());        for(int i=0;i<=20;i++){            UserInfo userInfo=new UserInfo();            userInfo.setId(i);            userInfo.setName("username"+i);            userInfo.setCarId(i);            userInfo.setJobId(i);            userInfo.setHomeId(i);            userInfoList.add(userInfo);        }        //stream 查詢一個用戶花費3s  並行計算後一個用戶1秒左右 查詢21個用戶花費21秒        //parallelStream 速度更慢        userInfoList.stream()                .map(userInfo->{                    QueryUserService queryUserService=new QueryUserService();                    userInfo =queryUserService.converUserInfo(userInfo);                    return userInfo;                }).collect(Collectors.toList());        System.out.println("=============");        long end=System.currentTimeMillis();        System.out.println(end-begin);    }}/<userinfo>/<void>/<void>/<string>/<string>/<string>/<string>/<string>/<string>/<queryutils>/<code>

看看這段代碼的—1,—2,—3部分,三個執行點的位置在使用了thenAccept組裝數據之後,還是可以避開串行化獲取數據的情況。只有在—4的位置才會發生堵塞。這樣對於性能的提升效果更佳。

這裡進行模擬測試,採用原始暴力手段查詢所消耗的時間是20 * 3 =60秒,但是這裡使用了CompletableFuture之後,查詢的時間就會縮短為了21秒。

結果:

<code>全部完成查詢=============21223/<code> 

這是一種使用了空間換時間的思路,或許你會說,異步查詢如果使用FutureTask是不是也可以呢。嗯嗯,是的,但是使用future有個問題,就是在於返回獲取異步結果的時候需要有等待狀態,這個等待的狀態是需要消耗時間進行堵塞的。

這裡我也做了關於使用普通FutureTask來執行查詢優化的結果:

<code> /**     * 使用 FutureTask 來優化查詢     *     * @param userInfo     * @return     */    public  UserInfo converUserInfoV2(UserInfo userInfo) {        Callable<string> homeCallable=new Callable() {            @Override            public Object call() throws Exception {                return queryUtilsSupplier.get().queryHome(userInfo.getHomeId());            }        };        FutureTask<string> getHomeDesc=new FutureTask<>(homeCallable);        new Thread(getHomeDesc).start();        futureMap.put("homeCallable",getHomeDesc);        Callable<string> carCallable=new Callable() {            @Override            public Object call() throws Exception {                return queryUtilsSupplier.get().queryCar(userInfo.getCarId());            }        };        FutureTask<string> getCarDesc=new FutureTask(carCallable);        new Thread(getCarDesc).start();        futureMap.put("carCallable",getCarDesc);        Callable<string> jobCallable=new Callable() {            @Override            public Object call() throws Exception {                return queryUtilsSupplier.get().queryCar(userInfo.getJobId());            }        };        FutureTask<string> getJobDesc=new FutureTask<>(jobCallable);        new Thread(getJobDesc).start();        futureMap.put("jobCallable",getJobDesc);        try {            userInfo.setHomeDes((String) futureMap.get("homeCallable").get());            userInfo.setCarDes((String)futureMap.get("carCallable").get());            userInfo.setJobDes((String)futureMap.get("jobCallable").get());        } catch (Exception e) {            e.printStackTrace();        }        System.out.println("該對象完成查詢" );        return userInfo;    }/<string>/<string>/<string>/<string>/<string>/<string>/<code>

經過測試,使用 futuretask 進行優化的查詢結果只有47s左右,遠遠不及CompletableFuture的性能高效.這是因為使用了futuretask的get方法依然是存在堵塞的情況。

關鍵部分看這段內容:

<code>            userInfo.setHomeDes((String) futureMap.get("homeCallable").get());  --1            userInfo.setCarDes((String)futureMap.get("carCallable").get());  --2            userInfo.setJobDes((String)futureMap.get("jobCallable").get());  --3/<code>

—1代碼在執行的時候遇到了堵塞,然後—2和—3的get也需要進行等待,因此使用常規的futuretask進行優化,這裡難免還是會有堵塞的情況。


分享到:


相關文章: