後端思維篇:手把手教你寫一個並行調用模板
前言
大家好,我是撿田螺的小男孩。
本文是後端思維專欄的第二篇哈。上一篇36個設計接口的錦囊,得到非常多小夥伴的認可。 36個設計接口的錦囊中也提到一個點:就是使用並行調用優化接口。所以接下來就快馬加鞭,寫第二篇:手把手教你寫一個並行調用模板。
- 一個串行調用的例子(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
二話不説,現在我們直接建一個
BaseTaskCommand
類,實現Callable
接口,把查詢用户信息、查詢banner信息、label標籤信息的查詢任務放進去。
代碼如下:
```
public class BaseTaskCommand implements Callable
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
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
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
實行並行調用。
本文大家學到了哪些知識呢?
1. 如何優化接口性能?某些場景下,可以使用並行調用代替串行。
2. 如何實現並行調用呢? 可以使用CompletionService
。
3. 學到的後端思維是? 日常開發中,要學會抽取通用的方法、或者工具。
4. 策略模式和工廠模式的應用
本文的話,設計模式這塊還不是很詳細,然後下一篇,給大家講講,我是如何在現有代碼基礎上,抽取設計模式的哈。然後,如果大家需要本文的完整代碼的話,可以關注我的公眾號:撿田螺的小男孩,裏面有我的聯繫方式哈。
- 程序員版本的八榮八恥,愛了
- 程序員版本的八榮八恥~
- 盤點數據庫慢查詢的12個原因
- 後端思維篇:手把手教你寫一個並行調用模板
- 設計好接口的36個錦囊
- 兩萬字詳解InnoDB的鎖
- 幾道高頻的JVM面試題
- Spring Boot 大屏展示,私活項目,已開源,接私活是真的香!
- oppo後端16連問
- 大事務問題到底要如何解決?
- 阿里面試:我們為什麼要分庫分表
- 三年半後端鵝廠面試經歷
- 2W字!詳解20道Redis經典面試題!(珍藏版)
- 小廠後端十連問
- SpringBoot 從入門到放棄全系列
- Redis主從、哨兵、 Cluster集羣一鍋端!
- 螞蟻金服一面:十道經典面試題解析
- 美團二面:Redis與MySQL雙寫一致性如何保證?
- 騰訊雲後端十五連問
- JVM調優總結