CompletableFuture
相比於jdk5所提出的future概念,future在執行的時候支持異步處理,但是在回調的過程中依舊是難免會遇到需要等待的情況。
在jdk8裏面,出現了CompletableFuture的新概念,支持對於異步處理完成任務之後自行處理數據。當發生異常的時候也能按照自定義的邏輯來處理。
如何通過使用CompletableFuture提升查詢的性能的問題呢?
下邊我舉個例子來演示:
首先我們定義一個UserInfo對象:
/**
* @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;
}
}
這個對象裏面的homeid,jobid,carid都是用於匹配對應的住房信息描述,職業信息描述,購車信息描述。
對於將id轉換爲描述信息的方式需要通過額外的sql查詢,這裏做了個簡單的工具類來進行模擬:
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";
}
}
這個工具類的功能看起來會比較通俗易懂,在常規的邏輯裏面,我們做批量對象的轉換大多數都是基於List遍歷,然後在循環裏面批量
查詢,這樣的方式並非說不行,而是顯得比較過於“暴力”。假設每次查詢需要消耗1s,那麼遍歷的一個size爲n的集合查詢消耗的時間就是n * 3s。
下邊來介紹一種更爲方便的技巧: CompletableFuture
定義一個QuerySupplier 實現Supplier接口,根據注入的類型進行轉譯查詢:
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;
}
}
由於對應的carid,homeid,jobid都需要到指定的k,v配置表裏面通過核心查詢包裝器來進行轉譯,因此通常的做法就是在for循環裏面一個個地進行遍歷解析,這樣的做法也比較易於理解。QuerySuppiler 是我寫的一個用於做對象解析的服務,代碼如下所示:
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);
}
}
看看這段代碼的—1,—2,—3部分,三個執行點的位置在使用了thenAccept組裝數據之後,還是可以避開串行化獲取數據的情況。只有在—4的位置纔會發生堵塞。這樣對於性能的提升效果更佳。
這裏進行模擬測試,採用原始暴力手段查詢所消耗的時間是20 * 3 =60秒,但是這裏使用了CompletableFuture之後,查詢的時間就會縮短爲了21秒。
結果:
全部完成查詢
=============
21223
這是一種使用了空間換時間的思路,或許你會說,異步查詢如果使用FutureTask是不是也可以呢。嗯嗯,是的,但是使用future有個問題,就是在於返回獲取異步結果的時候需要有等待狀態,這個等待的狀態是需要消耗時間進行堵塞的。
這裏我也做了關於使用普通FutureTask來執行查詢優化的結果:
/**
* 使用 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;
}
經過測試,使用 futuretask 進行優化的查詢結果只有47s左右,遠遠不及CompletableFuture的性能高效.這是因爲使用了futuretask的get方法依然是存在堵塞的情況。關鍵部分看這段內容:
userInfo.setHomeDes((String) futureMap.get("homeCallable").get()); --1
userInfo.setCarDes((String)futureMap.get("carCallable").get()); --2
userInfo.setJobDes((String)futureMap.get("jobCallable").get()); --3
—1代碼在執行的時候遇到了堵塞,然後—2和—3的get也需要進行等待,因此使用常規的futuretask進行優化,這裏難免還是會有堵塞的情況。