【轉】聊聊java高併發系統之非同步非阻塞
在做電商系統時,流量入口如首頁、活動頁、商品詳情頁等系統承載了網站的大部分流量,而這些系統的主要職責包括聚合資料拼裝模板、熱點統計、快取、下游功能降級開關、託底資料等等。其中聚合資料需要呼叫其它多個系統服務獲取資料、拼裝資料/模板然後返回給前端,聚合資料來源主要有依賴系統/服務、快取、資料庫等;而系統之間的呼叫可以通過如http介面呼叫(如HttpClient)、SOA服務呼叫(如dubbo、thrift)等等。
在Java中,如使用Tomcat,一個請求會分配一個執行緒進行請求處理,該執行緒負責獲取資料、拼裝資料或模板然後返回給前端;在同步呼叫獲取資料介面的情況下(等待依賴系統返回資料),整個執行緒是一直被佔用並阻塞的。如果有大量的這種請求,每個請求佔用一個執行緒,但執行緒一直處於阻塞,降低了系統的吞吐量,這將導致應用的吞吐量下降;我們希望在呼叫依賴的服務響應比較慢,此時應該讓出執行緒和CPU來處理下一個請求,當依賴的服務返回了再分配相應的執行緒來繼續處理。而這應該有更好的解決方案:非同步/協程。而Java是不支援協程的(雖然有些Java框架說支援,但還是高層API的封裝),因此在Java中我們還可以使用非同步來提升吞吐量。目前java一些開源框架(HttpClient\HttpAsyncClient、dubbo、thrift等等)大部分都支援。
幾種呼叫方式
同步阻塞呼叫
即序列呼叫,響應時間為所有服務的響應時間總和;
半非同步(非同步Future)
執行緒池,非同步Future,使用場景:併發請求多服務,總耗時為最長響應時間;提升總響應時間,但是阻塞主請求執行緒,高併發時依然會造成執行緒數過多,CPU上下文切換;
全非同步(Callback)
Callback方式呼叫,使用場景:不考慮回撥時間且只能對結果做簡單處理,如果依賴服務是兩個或兩個以上服務,則不能合併兩個服務的處理結果;不阻塞主請求執行緒,但使用場景有限。
非同步回撥鏈式編排
非同步回撥鏈式編排(JDK8 CompletableFuture),使用場景:其實不是非同步呼叫方式,只是對依賴多服務的Callback呼叫結果處理做結果編排,來彌補Callback的不足,從而實現全非同步鏈式呼叫。
接下來看看如何設計利用全非同步Callback呼叫和非同步回撥鏈式編排處理結果來實現全非同步系統設計。
同步阻塞呼叫
- public class Test {
- public static void main(String[] args) throws Exception {
- RpcService rpcService = new RpcService();
- HttpService httpService = new HttpService();
- //耗時10ms
- Map<String, String>result1 = rpcService
- //耗時20ms
- Integer result2 = httpService.getHttpResult();
- //總耗時30ms
- }
- static class RpcService {
- Map<String, String> getRpcResult() throws Exception {
- //呼叫遠端方法(遠端方法耗時約10ms,可以使用Thread.sleep模擬)
- }
- }
- static class HttpService {
- Integer getHttpResult() throws Exception {
- //呼叫遠端方法(遠端方法耗時約20ms,可以使用Thread.sleep模擬)
- Thread.sleep(20);
- return 0;
- }
- }
- }
半非同步(非同步Future)
- public class Test {
- final static ExecutorService executor = Executors.newFixedThreadPool(2);
- public static void main(String[] args) {
- RpcService rpcService = new RpcService();
- HttpService httpService = new HttpService();
- Future<Map<String, String>>future1 = null;
- Future<Integer>future2 = null;
- try {
- future1 = executor.submit(() -> rpcService.getRpcResult());
- future2 = executor.submit(() -> httpService.getHttpResult());
- //耗時10ms
- Map<String, String>result1 = future1.get(300, TimeUnit.MILLISECONDS);
- //耗時20ms
- Integer result2 = future2.get(300, TimeUnit.MILLISECONDS);
- //總耗時20ms
- } catch (Exception e) {
- if (future1 != null) {
- future1.cancel(true);
- }
- if (future2 != null) {
- future2.cancel(true);
- }
- throw new RuntimeException(e);
- }
- }
- static class RpcService {
- Map<String, String> getRpcResult() throws Exception {
- //呼叫遠端方法(遠端方法耗時約10ms,可以使用Thread.sleep模擬)
- }
- }
- static class HttpService {
- Integer getHttpResult() throws Exception {
- //呼叫遠端方法(遠端方法耗時約20ms,可以使用Thread.sleep模擬)
- }
- }
- }
全非同步(Callback)
- public class AsyncTest {
- public staticHttpAsyncClient httpAsyncClient;
- public static CompletableFuture<String> getHttpData(String url) {
- CompletableFuture asyncFuture = new CompletableFuture();
- HttpPost post = new HttpPost(url);
- HttpAsyncRequestProducer producer = HttpAsyncMethods.create(post);
- AsyncCharConsumer<HttpResponse>consumer = newAsyncCharConsumer<HttpResponse>() {
- HttpResponse response;
- protected HttpResponse buildResult(final HttpContext context) {
- return response;
- }
- …...
- };
- FutureCallback callback = new FutureCallback<HttpResponse>() {
- public void completed(HttpResponse response) {
- asyncFuture.complete(EntityUtils.toString(response.getEntity()));
- }
- …...
- };
- httpAsyncClient.execute(producer, consumer, callback);
- return asyncFuture;
- }
- public static void main(String[] args) throws Exception {
- AsyncTest.getHttpData("http://www.jd.com");
- Thread.sleep(1000000);
- }
- }
本示例使用HttpAsyncClient演示。
非同步回撥鏈式編排
CompletableFuture提供了50多個API,可以滿足所需的各種場景的非同步處理的編排,在此列舉三個場景:
場景1:三個服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒;
方法test1:
- public static void test1() throws Exception {
- HelloClientDemoTest service = new HelloClientDemoTest();
- /**
- * 場景1 兩個以上服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒
- * 並且兩個服務也是非同步非阻塞呼叫
- */
- CompletableFuture future1 = service.getHttpData("http://www.jd.com");
- CompletableFuture future2 = service.getHttpData("http://www.jd.com");
- CompletableFuture future3 =service.getHttpData("http://www.jd.com");
- List<CompletableFuture>futureList = Lists.newArrayList(future1,future2, future3);
- CompletableFuture<Void>allDoneFuture =CompletableFuture.allOf(futureList.toArray(newCompletableFuture[futureList.size()]));
- CompletableFuture<String>future4 =allDoneFuture.thenApply(v -> {
- List<Object>result =futureList.stream().map(CompletableFuture::join)
- .collect(Collectors.toList());
- //注意順序
- String result1 = (String)result.get(0);
- String result2 = (String)result.get(1);
- String result3 = (String)result.get(2);
- //處理業務....
- return result1 + result2 + result3;
- }).exceptionally(e -> {
- //e.printStackTrace();
- return "";
- });
- //返回
- }
場景2、兩個服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒;
方法test2:
- public void test2() throws Exception {
- HelloClientDemoTest service = new HelloClientDemoTest();
- /**
- * 場景2 兩個介面併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒
- * 並且兩個服務也是非同步非阻塞呼叫
- */
- CompletableFuture future1 = service.getHttpData("http://www.jd.com");
- CompletableFuture future2 =service.getHttpData("http://www.jd.com");
- CompletableFuture future3 =future1.thenCombine(future2, (f1, f2) -> {
- //處理業務....
- return f1 + "," + f2;
- }).exceptionally(e -> {
- return "";
- });
- //返回
- }
場景3、兩個服務,併發非同步呼叫兩個服務,並且一個服務的結果返回後再次呼叫另一服務,然後將三個結果後並處理,返回CompletableFuture,整個處理過程中不阻塞任何執行緒;
方法test3:
- publicvoid test3() throws Exception {
- HelloClientDemoTest service = new HelloClientDemoTest();
- /**
- * 場景3 兩請求依賴呼叫,然後與另一服務結果組合處理,返回CompletableFuture,不阻塞主執行緒
- * 並且兩個服務也是非同步非阻塞呼叫
- */
- CompletableFuture future1 = service.getHttpData("http://www.jd.com");
- CompletableFuture future2 = service.getHttpData("http://www.jd.com");
- CompletableFuture<String>future3= future1.thenApply((param) -> {
- CompletableFuture future4 =service.getHttpData("http://www.jd.com");
- return future4;
- });
- CompletableFuture future5 =future2.thenCombine(future3, (f2, f3) -> {
- //....處理業務
- return f2 + "," + f3;
- }).exceptionally(e -> {
- return "";
- });
- //返回future5
- }
全非同步Web系統設計
主要技術:servlet3,JDK8 CompletableFuture,支援非同步Callback呼叫的RPC框架。
先看一下處理流程圖:
servlet3:Servlet 接收到請求之後,可能首先需要對請求攜帶的資料進行一些預處理;接著,Servlet 執行緒將請求轉交給一個非同步執行緒來執行業務處理,執行緒本身返回至容器。針對業務處理較耗時的情況,這將大大減少伺服器資源的佔用,並且提高併發處理速度。servlet3可參考商品詳情頁系統的Servlet3非同步化實踐,結合其中講解的servlet3整合:
- public void submitFuture(finalHttpServletRequest req, final Callable<CompletableFuture> task) throwsException{
- final String uri = req.getRequestURI();
- final Map<String, String[]>params = req.getParameterMap();
- final AsyncContext asyncContext = req.startAsync();
- asyncContext.getRequest().setAttribute("uri", uri);
- asyncContext.getRequest().setAttribute("params", params);
- asyncContext.setTimeout(asyncTimeoutInSeconds * 1000);
- if(asyncListener != null) {
- asyncContext.addListener(asyncListener);
- }
- CompletableFuture future = task.call();
- future.thenAccept(result -> {
- HttpServletResponse resp = (HttpServletResponse)asyncContext.getResponse();
- try {
- if(result instanceof String) {
- byte[] bytes = new byte[0];
- if (StringUtils.isBlank(result)){
- resp.setContentType("text/html;charset=gbk");
- resp.setContentLength(0);
- } else {
- bytes =result.getBytes("GBK");
- }
- //resp.setBufferSize(bytes.length);
- resp.setContentType("text/html;charset=gbk");
- if(StringUtils.isNotBlank(localIp)) {
- resp.setHeader("t.ser", localIp);
- }
- resp.setContentLength(bytes.length);
- resp.getOutputStream().write(bytes);
- } else {
- write(resp,JSONUtils.toJSON(result));
- }
- } catch (Throwable e) {
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); //程式內部錯誤
- try {
- LOG.error("get infoerror, uri : {}, params : {}", uri,JSONUtils.toJSON(params), e);
- } catch (Exception ex) {
- }
- } finally {
- asyncContext.complete();
- }
- }).exceptionally(e -> {
- asyncContext.complete();
- return null;
- });
- }
另外還有Java中協程庫Quasar,可參考《Java的纖程庫 - Quasar》,目前沒有在應用中使用並在測試FiberHttpServlet的時候遇到很多坑,日後把Quasar自如運用後形成日記,希望能結實更多的朋友一起研究,踩坑。
作者:孫偉,目前負責京東商品詳情頁統一服務系統,寫過java,寫過ngx_lua,還寫過storm等,喜歡學習研究新事物。
【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的部落格),公眾號id: kaitao-1234567】