當資料量較大的時候,都會透過分庫分表來拆分,分擔讀寫的壓力。分庫分表後比較麻煩的就是查詢的問題,如果不是直接根據分片鍵去查詢的話,需要對多個表進行查詢。
在一些複雜的業務場景下,比如訂單搜尋,除了訂單號,使用者,商家 這些常用的搜尋條件,可能還有時間,商品等等。
目前常見的做法將資料同步到ES這類搜尋框架中進行查詢,然後透過搜出來的結果,一般是主鍵ID, 再去具體的資料表中查詢完整的資料,組裝返回給呼叫方。
比如下面這段程式碼,首先查詢出文章資訊,然後根據文章中的使用者ID去查詢使用者的暱稱。
List<ArticleBO> articleBos = articleDoPage.getRecords().stream().map(r -> { String nickname = userManager.getNickname(r.getUserId()); return articleBoConvert.convertPlus(r, nickname);}).collect(Collectors.toList());
如果文章有10條資料,那麼就需要呼叫10次使用者服務提供的介面,而且是同步呼叫操作。
當然我們也可以用並行流來實現併發呼叫,程式碼如下:
List<ArticleBO> articleBos = articleDoPage.getRecords().parallelStream().map(r -> { String nickname = userManager.getNickname(r.getUserId()); return articleBoConvert.convertPlus(r, nickname);}).collect(Collectors.toList());
並行流的優點很明顯,程式碼不用做特別大的改動。需要注意如果用並行流,最好單獨定義一個ForkJoinPool。
除了用並行流,還可以使用批次查詢的方式來提高效能,降低RPC的呼叫次數,程式碼如下:
List<Long> userIds = articleDoPage.getRecords().stream().map(article -> article.getUserId()).collect(Collectors.toList());Map<Long, String> nickNameMap = userManager.queryByIds(userIds).stream().collect(Collectors.toMap(UserResponse::getId, UserResponse::getNickname));List<ArticleBO> articleBos = articleDoPage.getRecords().stream().map(r -> { String nickname = nickNameMap.containsKey(r.getUserId()) ? nickNameMap.get(r.getUserId()) : CommonConstant.DEFAULT_EMPTY_STR; return articleBoConvert.convertPlus(r, nickname);}).collect(Collectors.toList());
但批次查詢還是同步模式,下面介紹如果使用CompletableFuture來實現非同步併發呼叫,直接用原生的CompletableFuture也可以,但是編排能力沒有那麼強,這裡我們選擇一款基於CompletableFuture封裝的並行編排框來實現,詳細介紹檢視我之前的這篇文章:https://mp.weixin.qq.com/s/3EE8ccydK16gC1oY4AWnoA
稍微做了下封裝,提供了更方便使用的工具類來實現併發呼叫多個介面的邏輯。
第一種方式,適用於比如從ES查出了一批ID, 然後根據ID去資料庫中或者呼叫RPC查詢真實資料,最後得到一個Map,可以根據Key獲取對應的資料。
內部是多執行緒併發呼叫,會等到結果全部返回。
public Object aggregationApi() { long s = System.currentTimeMillis(); List<String> ids = new ArrayList<>(); ids.add("1"); ids.add("2"); ids.add("3"); Map<String, UserResponse> callResult = AsyncTemplate.call(ids, id -> { return userService.getUser(id); }, u -> u.getId(), COMMON_POOL); long e = System.currentTimeMillis(); System.out.println("耗時:" + (e-s) + "ms"); return "";}
另一個場景就是API聚合的場景,需要並行呼叫多個介面,將結果進行組裝。
List<AsyncCall> params = new ArrayList<>();AsyncCall<Integer, Integer> goodsQuery = new AsyncCall("goodsQuery", 1);params.add(goodsQuery);AsyncCall<String, OrderResponse> orderQuery = new AsyncCall("orderQuery", "100");params.add(orderQuery);UserQuery q = new UserQuery();q.setAge(18);q.setName("yinjihuan");AsyncCall<UserQuery, UserResponse> userQuery = new AsyncCall("userQuery", q);params.add(userQuery);AsyncTemplate.call(params, p -> { if (p.getTaskId().equals("goodsQuery")) { AsyncCall<Integer, Integer> query = p; return goodsService.getGoodsName(query.getParam()); } if (p.getTaskId().equals("orderQuery")) { AsyncCall<String, OrderResponse> query = p; return orderService.getOrder(query.getParam()); } if (p.getTaskId().equals("userQuery")) { AsyncCall<UserQuery, UserResponse> query = p; return userService.getUser(query.getParam()); } return null;});
AsyncCall中定義引數和響應的型別,響應結果會在執行完後會自動設定到AsyncCall中。在call方法中需要根據taskId去做對應的處理邏輯,不同的taskId呼叫的介面不一樣。
原始碼參考:https://github.com/yinjihuan/kitty