前言:
本文主要介紹內容有:
- 一個串行調用的例子(App首頁信息查詢)
- CompletionService實現并行調用
- 抽取通用的并行調用方法
- 代碼思考以及設計模式應用
- 思考總結
1. 一個串行調用的例子
如果讓你設計一個APP首頁查詢的接口,它需要查用戶信息、需要查banner
信息、需要查標簽信息等等。
一般情況,小伙伴會實現如下:
public AppHeadInfoResponse queryAppHeadInfo(AppInfoReq req) {
//查用戶信息
UserInfoParam userInfoParam = buildUserParam(req);
UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
//查banner信息
BannerParam bannerParam = buildBannerParam(req);
BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
//查標簽信息
LabelParam labelParam = buildLabelParam(req);
LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
//組裝結果
return buildResponse(userInfoDTO,bannerDTO,labelDTO);
}
這段代碼會有什么問題嘛? 其實這是一段挺正常的代碼,但是這個方法實現中,查詢用戶、banner、標簽信息,是串行的,如果查詢用戶信息200ms
,查詢banner信息100ms
,查詢標簽信息200ms
的話,耗時就是500ms
啦。
其實為了優化性能,我們可以修改為并行調用的方式,耗時可以降為200ms
,如下圖所示:
2. CompletionService實現并行調用
對于上面的例子,如何實現并行調用呢?
有小伙伴說,可以使用Future+Callable
實現多個任務的并行調用。但是線程池執行批量任務時,返回值用Future的get()
獲取是阻塞的,如果前一個任務執行比較耗時的話,get()
方法會阻塞,形成排隊等待的情況。
而CompletionService
是對定義ExecutorService
進行了包裝,可以一邊生成任務,一邊獲取任務的返回值。讓這兩件事分開執行,任務之間不會互相阻塞,可以獲取最先完成的任務結果。
CompletionService
的實現原理比較簡單,底層通過FutureTask+阻塞隊列,實現了任務先完成的話,可優先獲取到。也就是說任務執行結果按照完成的先后順序來排序,先完成可以優化獲取到。內部有一個先進先出的阻塞隊列,用于保存已經執行完成的Future,你調用CompletionService
的poll或take方法即可獲取到一個已經執行完成的Future,進而通過調用Future接口實現類的get
方法獲取最終的結果。
接下來,我們來看下,如何用CompletionService
,實現并行查詢APP首頁信息哈。
思考步驟如下:
我們先把查詢用戶信息的任務,放到線程池,如下:
ExecutorService executor = Executors.newFixedThreadPool(10);
//查詢用戶信息
CompletionService<UserInfoDTO> userDTOCompletionService = new ExecutorCompletionService<UserInfoDTO>(executor);
Callable<UserInfoDTO> userInfoDTOCallableTask = () -> {
UserInfoParam userInfoParam = buildUserParam(req);
return userService.queryUserInfo(userInfoParam);
};
userDTOCompletionService.submit(userInfoDTOCallableTask);
- 但是如果想把查詢
banner
信息的任務,也放到這個線程池的話,發現不好放了,因為返回類型不一樣,一個是UserInfoDTO
,另外一個是BannerDTO
。那這時候,我們是不是把泛型聲明為Object即可,因為所有對象都是繼承于Object的?如下:
ExecutorService executor = Executors.newFixedThreadPool(10);
//查詢用戶信息
CompletionService<Object> baseDTOCompletionService = new ExecutorCompletionService<Object>(executor);
Callable<Object> userInfoDTOCallableTask = () -> {
UserInfoParam userInfoParam = buildUserParam(req);
return userService.queryUserInfo(userInfoParam);
};
//banner信息任務
Callable<Object> bannerDTOCallableTask = () -> {
BannerParam bannerParam = buildBannerParam(req);
return bannerService.queryBannerInfo(bannerParam);
};
//提交用戶信息任務
baseDTOCompletionService.submit(userInfoDTOCallableTask);
//提交banner信息任務
baseDTOCompletionService.submit(bannerDTOCallableTask);
- 這里會有個問題,就是獲取返回值的時候,我們不知道哪個
Object
是用戶信息的DTO,哪個是BannerDTO
?怎么辦呢?這時候,我們可以在參數里面做個擴展嘛,即參數聲明為一個基礎對象BaseRspDTO,再搞個泛型放Object數據的,然后基礎對象BaseRspDTO有個區分是UserDTO還是BannerDTO的唯一標記屬性key。代碼如下:
public class BaseRspDTO<T extends Object> {
//區分是DTO返回的唯一標記,比如是UserInfoDTO還是BannerDTO
private String key;
//返回的data
private T data;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
//并行查詢App首頁信息
public AppHeadInfoResponse parallelQueryAppHeadPageInfo(AppInfoReq req) {
long beginTime = System.currentTimeMillis();
System.out.println("開始并行查詢app首頁信息,開始時間:" + beginTime);
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);
//查詢用戶信息任務
Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> {
UserInfoParam userInfoParam = buildUserParam(req);
UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
userBaseRspDTO.setKey("userInfoDTO");
userBaseRspDTO.setData(userInfoDTO);
return userBaseRspDTO;
};
//banner信息查詢任務
Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> {
BannerParam bannerParam = buildBannerParam(req);
BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
bannerBaseRspDTO.setKey("bannerDTO");
bannerBaseRspDTO.setData(bannerDTO);
return bannerBaseRspDTO;
};
//label信息查詢任務
Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> {
LabelParam labelParam = buildLabelParam(req);
LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
labelBaseRspDTO.setKey("labelDTO");
labelBaseRspDTO.setData(labelDTO);
return labelBaseRspDTO;
};
//提交用戶信息任務
baseDTOCompletionService.submit(userInfoDTOCallableTask);
//提交banner信息任務
baseDTOCompletionService.submit(bannerDTOCallableTask);
//提交label信息任務
baseDTOCompletionService.submit(labelDTODTOCallableTask);
UserInfoDTO userInfoDTO = null;
BannerDTO bannerDTO = null;
LabelDTO labelDTO = null;
try {
//因為提交了3個任務,所以獲取結果次數是3
for (int i = 0; i < 3; i++) {
Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(1, TimeUnit.SECONDS);
BaseRspDTO baseRspDTO = baseRspDTOFuture.get();
if ("userInfoDTO".equals(baseRspDTO.getKey())) {
userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
} else if ("bannerDTO".equals(baseRspDTO.getKey())) {
bannerDTO = (BannerDTO) baseRspDTO.getData();
} else if ("labelDTO".equals(baseRspDTO.getKey())) {
labelDTO = (LabelDTO) baseRspDTO.getData();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("結束并行查詢app首頁信息,總耗時:" + (System.currentTimeMillis() - beginTime));
return buildResponse(userInfoDTO, bannerDTO, labelDTO);
}
到這里為止,一個基于CompletionService
實現并行調用的例子已經實現啦。是不是很開心,哈哈。
3. 抽取通用的并行調用方法
我們回過來觀察下第2小節,查詢app首頁信息的demo:CompletionService
實現了并行調用。大家有沒有什么其他想法呢?比如,假設別的業務場景,也想通過并行調用優化,那是不是也得搞一套類似第2小節的代碼。所以,我們是不是可以抽取一個通用的并行方法,讓別的場景也可以用,對吧?這就是后端思維啦!
基于第2小節的代碼,我們如何抽取通用并行調用方法呢。
首先,這個通用的并行調用方法,不能跟業務相關的屬性掛鉤,對吧,所以方法的入參應該有哪些呢?
方法的入參,可以有
Callable
對吧。因為并行,肯定是多個Callable任務的。所以,入參應該是一個Callable
的數組。再然后,基于上面的APP首頁查詢的例子,Callable
里面得帶BaseRspDTO
泛型,對吧?因此入參就是List<Callable<BaseRspDTO<Object>>> list
。
那并行調用的出參呢? 你有多個Callable
的任務,是不是得有多個對應的返回,因此,你的出參可以是List<BaseRspDTO<Object>>
。我們抽取的通用并行調用模板,就可以寫成醬紫:
public List<BaseRspDTO<Object>> executeTask(List<Callable<BaseRspDTO<Object>>> taskList) {
List<BaseRspDTO<Object>> resultList = new ArrayList<>();
//校驗參數
if (taskList == null || taskList.size() == 0) {
return resultList;
}
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);
//提交任務
for (Callable<BaseRspDTO<Object>> task : taskList) {
baseDTOCompletionService.submit(task);
}
try {
//遍歷獲取結果
for (int i = 0; i < taskList.size(); i++) {
Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(2, TimeUnit.SECONDS);
resultList.add(baseRspDTOFuture.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return resultList;
}
既然我們是抽取通用的并行調用方法,那以上的方法是否還有哪些地方需要改進的呢?
- 第一個可以優化的地方,就是
executor線程池
,比如有些業務場景想用A線程池
,有些業務想用B線程池
,那么,這個方法,就不通用啦,對吧。我們可以把線程池以參數的實行提供出來,給調用方自己控制。 - 第二個可以優化的地方,就是
CompletionService
的poll
方法獲取時,超時時間是寫死的。因為不同業務場景,超時時間可能不一樣。所以,超時時間也是可以以參數形式放出來,給調用方自己控制。
我們再次優化一下這個通用的并行調用模板,代碼如下:
public List<BaseRspDTO<Object>> executeTask(List<Callable<BaseRspDTO<Object>>> taskList, long timeOut, ExecutorService executor) {
List<BaseRspDTO<Object>> resultList = new ArrayList<>();
//校驗參數
if (taskList == null || taskList.size() == 0) {
return resultList;
}
if (executor == null) {
return resultList;
}
if (timeOut <= 0) {
return resultList;
}
//提交任務
CompletionService<BaseRspDTO<Object>> baseDTOCompletionService = new ExecutorCompletionService<BaseRspDTO<Object>>(executor);
for (Callable<BaseRspDTO<Object>> task : taskList) {
baseDTOCompletionService.submit(task);
}
try {
//遍歷獲取結果
for (int i = 0; i < taskList.size(); i++) {
Future<BaseRspDTO<Object>> baseRspDTOFuture = baseDTOCompletionService.poll(timeOut, TimeUnit.SECONDS);
resultList.add(baseRspDTOFuture.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return resultList;
}
以后別的場景也需要用到并行調用的話,直接調用你的這個方法即可,是不是有點小小的成就感啦,哈哈。
4. 代碼思考以及設計模式應用
我們把抽取的那個公用的并行調用方法,應用到App首頁信息查詢
的例子,
代碼如下:
public AppHeadInfoResponse parallelQueryAppHeadPageInfo1(AppInfoReq req) {
long beginTime = System.currentTimeMillis();
System.out.println("開始并行查詢app首頁信息,開始時間:" + beginTime);
//用戶信息查詢任務
Callable<BaseRspDTO<Object>> userInfoDTOCallableTask = () -> {
UserInfoParam userInfoParam = buildUserParam(req);
UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
userBaseRspDTO.setKey("userInfoDTO");
userBaseRspDTO.setData(userInfoDTO);
return userBaseRspDTO;
};
//banner信息查詢任務
Callable<BaseRspDTO<Object>> bannerDTOCallableTask = () -> {
BannerParam bannerParam = buildBannerParam(req);
BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
bannerBaseRspDTO.setKey("bannerDTO");
bannerBaseRspDTO.setData(bannerDTO);
return bannerBaseRspDTO;
};
//label信息查詢任務
Callable<BaseRspDTO<Object>> labelDTODTOCallableTask = () -> {
LabelParam labelParam = buildLabelParam(req);
LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
labelBaseRspDTO.setKey("labelDTO");
labelBaseRspDTO.setData(labelDTO);
return labelBaseRspDTO;
};
List<Callable<BaseRspDTO<Object>>> taskList = new ArrayList<>();
taskList.add(userInfoDTOCallableTask);
taskList.add(bannerDTOCallableTask);
taskList.add(labelDTODTOCallableTask);
ExecutorService executor = Executors.newFixedThreadPool(10);
List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor);
if (resultList == null || resultList.size() == 0) {
return new AppHeadInfoResponse();
}
UserInfoDTO userInfoDTO = null;
BannerDTO bannerDTO = null;
LabelDTO labelDTO = null;
//遍歷結果
for (int i = 0; i < resultList.size(); i++) {
BaseRspDTO baseRspDTO = resultList.get(i);
if ("userInfoDTO".equals(baseRspDTO.getKey())) {
userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
} else if ("bannerDTO".equals(baseRspDTO.getKey())) {
bannerDTO = (BannerDTO) baseRspDTO.getData();
} else if ("labelDTO".equals(baseRspDTO.getKey())) {
labelDTO = (LabelDTO) baseRspDTO.getData();
}
}
System.out.println("結束并行查詢app首頁信息,總耗時:" + (System.currentTimeMillis() - beginTime));
return buildResponse(userInfoDTO, bannerDTO, labelDTO);
}
基于以上代碼,小伙伴們,是否還有其他方面的優化想法呢? 比如這幾個Callable
查詢任務,我們是不是也可以抽取一下?讓代碼更加簡潔。
二話不說,現在我們直接建一個BaseTaskCommand
類,實現Callable
接口,把查詢用戶信息、查詢banner信息、label標簽信息的查詢任務放進去。
代碼如下:
public class BaseTaskCommand implements Callable<BaseRspDTO<Object>> {
private String key;
private AppInfoReq req;
private IUserService userService;
private IBannerService bannerService;
private ILabelService labelService;
public BaseTaskCommand(String key, AppInfoReq req, IUserService userService, IBannerService bannerService, ILabelService labelService) {
this.key = key;
this.req = req;
this.userService = userService;
this.bannerService = bannerService;
this.labelService = labelService;
}
@Override
public BaseRspDTO<Object> call() throws Exception {
if ("userInfoDTO".equals(key)) {
UserInfoParam userInfoParam = buildUserParam(req);
UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
userBaseRspDTO.setKey("userInfoDTO");
userBaseRspDTO.setData(userInfoDTO);
return userBaseRspDTO;
} else if ("bannerDTO".equals(key)) {
BannerParam bannerParam = buildBannerParam(req);
BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
bannerBaseRspDTO.setKey("bannerDTO");
bannerBaseRspDTO.setData(bannerDTO);
return bannerBaseRspDTO;
} else if ("labelDTO".equals(key)) {
LabelParam labelParam = buildLabelParam(req);
LabelDTO labelDTO = labelService.queryLabelInfo(labelParam);
BaseRspDTO<Object> labelBaseRspDTO = new BaseRspDTO<Object>();
labelBaseRspDTO.setKey("labelDTO");
labelBaseRspDTO.setData(labelDTO);
return labelBaseRspDTO;
}
return null;
}
private UserInfoParam buildUserParam(AppInfoReq req) {
return new UserInfoParam();
}
private BannerParam buildBannerParam(AppInfoReq req) {
return new BannerParam();
}
private LabelParam buildLabelParam(AppInfoReq req) {
return new LabelParam();
}
}
以上這塊代碼,構造函數還是有比較多的參數,并且call()
方法中,有多個if...else...
,如果新增一個分支(比如查詢浮層信息),那又得在call
方法里修改了,并且BaseTaskCommand的構造器也要修改了。
大家是否有印象,多程序中出現多個if...else...時,我們就可以考慮使用策略模式+工廠模式優化。
我們聲明多個策略實現類,如下:
public interface IBaseTask {
//返回每個策略類的key,如
String getTaskType();
BaseRspDTO<Object> execute(AppInfoReq req);
}
//用戶信息策略類
@Service
public class UserInfoStrategyTask implements IBaseTask {
@Autowired
private IUserService userService;
@Override
public String getTaskType() {
return "userInfoDTO";
}
@Override
public BaseRspDTO<Object> execute(AppInfoReq req) {
UserInfoParam userInfoParam = userService.buildUserParam(req);
UserInfoDTO userInfoDTO = userService.queryUserInfo(userInfoParam);
BaseRspDTO<Object> userBaseRspDTO = new BaseRspDTO<Object>();
userBaseRspDTO.setKey(getTaskType());
userBaseRspDTO.setData(userBaseRspDTO);
return userBaseRspDTO;
}
}
/**
* banner信息策略實現類
**/
@Service
public class BannerStrategyTask implements IBaseTask {
@Autowired
private IBannerService bannerService;
@Override
public String getTaskType() {
return "bannerDTO";
}
@Override
public BaseRspDTO<Object> execute(AppInfoReq req) {
BannerParam bannerParam = bannerService.buildBannerParam(req);
BannerDTO bannerDTO = bannerService.queryBannerInfo(bannerParam);
BaseRspDTO<Object> bannerBaseRspDTO = new BaseRspDTO<Object>();
bannerBaseRspDTO.setKey(getTaskType());
bannerBaseRspDTO.setData(bannerDTO);
return bannerBaseRspDTO;
}
}
...
然后這幾個策略實現類,怎么交給spring
管理呢? 我們可以實現ApplicationContextAware
接口,把策略的實現類注入到一個map,然后根據請求方不同的策略請求類型(即DTO的類型),去實現不同的策略類調用。其實這類似于工廠模式的思想。
代碼如下:
/**
* 策略工廠類
**/
@Component
public class TaskStrategyFactory implements ApplicationContextAware {
private Map<String, IBaseTask> map = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IBaseTask> tempMap = applicationContext.getBeansOfType(IBaseTask.class);
tempMap.values().forEach(iBaseTask -> {
map.put(iBaseTask.getTaskType(), iBaseTask);
});
}
public BaseRspDTO<Object> executeTask(String key, AppInfoReq req) {
IBaseTask baseTask = map.get(key);
if (baseTask != null) {
System.out.println("工廠策略實現類執行");
return baseTask.execute(req);
}
return null;
}
}
有了策略工廠類TaskStrategyFactory
,我們再回來優化下BaseTaskCommand
類的代碼。它的構造器已經不需要多個IUserService userService, IBannerService bannerService, ILabelService labelService
啦,只需要策略工廠類TaskStrategyFactory
即可。同時策略也不需要多個if...else...
判斷了,用策略工廠類TaskStrategyFactory
代替即可。
優化后的代碼如下:
public class BaseTaskCommand implements Callable<BaseRspDTO<Object>> {
private String key;
private AppInfoReq req;
private TaskStrategyFactory taskStrategyFactory;
public BaseTaskCommand(String key, AppInfoReq req, TaskStrategyFactory taskStrategyFactory) {
this.key = key;
this.req = req;
this.taskStrategyFactory = taskStrategyFactory;
}
@Override
public BaseRspDTO<Object> call() throws Exception {
return taskStrategyFactory.executeTask(key, req);
}
}
因此整個app首頁信息并行
查詢,就可以優化成這樣啦,如下:
public AppHeadInfoResponse parallelQueryAppHeadPageInfo2(AppInfoReq req) {
long beginTime = System.currentTimeMillis();
System.out.println("開始并行查詢app首頁信息(最終版本),開始時間:" + beginTime);
List<Callable<BaseRspDTO<Object>>> taskList = new ArrayList<>();
//用戶信息查詢任務
taskList.add(new BaseTaskCommand("userInfoDTO", req, taskStrategyFactory));
//banner查詢任務
taskList.add(new BaseTaskCommand("bannerDTO", req, taskStrategyFactory));
//標簽查詢任務
taskList.add(new BaseTaskCommand("labelDTO", req, taskStrategyFactory));
ExecutorService executor = Executors.newFixedThreadPool(10);
List<BaseRspDTO<Object>> resultList = parallelInvokeCommonService.executeTask(taskList, 3, executor);
if (resultList == null || resultList.size() == 0) {
return new AppHeadInfoResponse();
}
UserInfoDTO userInfoDTO = null;
BannerDTO bannerDTO = null;
LabelDTO labelDTO = null;
for (BaseRspDTO<Object> baseRspDTO : resultList) {
if ("userInfoDTO".equals(baseRspDTO.getKey())) {
userInfoDTO = (UserInfoDTO) baseRspDTO.getData();
} else if ("bannerDTO".equals(baseRspDTO.getKey())) {
bannerDTO = (BannerDTO) baseRspDTO.getData();
} else if ("labelDTO".equals(baseRspDTO.getKey())) {
labelDTO = (LabelDTO) baseRspDTO.getData();
}
}
System.out.println("結束并行查詢app首頁信息(最終版本),總耗時:" + (System.currentTimeMillis() - beginTime));
return buildResponse(userInfoDTO, bannerDTO, labelDTO);
}
5. 思考總結
以上代碼整體優化下來,已經很簡潔啦。那還有沒有別的優化思路呢。
其實還是有的,比如,把唯一標記的key
定義為枚舉,而不是寫死的字符串"userInfoDTO"、"bannerDTO","labelDTO"
。還有,除了CompletionService
,有些小伙伴喜歡用CompletableFuture
實行并行調用。
本文大家學到了哪些知識呢?
- 如何優化接口性能?某些場景下,可以使用并行調用代替串行。
- 如何實現并行調用呢? 可以使用
CompletionService
。 - 學到的后端思維是? 日常開發中,要學會抽取通用的方法、或者工具。
- 策略模式和工廠模式的應用
到此這篇關于利用Java代碼寫一個并行調用模板的文章就介紹到這了,更多相關Java并行調用模板內容請搜索html5模板網以前的文章希望大家以后多多支持html5模板網!