1. 程式人生 > >【轉】聊聊java高併發系統之非同步非阻塞

【轉】聊聊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呼叫和非同步回撥鏈式編排處理結果來實現全非同步系統設計。

同步阻塞呼叫

  1. public class Test { 
  2.    public static void main(String[] args) throws Exception { 
  3.        RpcService rpcService = new RpcService(); 
  4.        HttpService httpService = new HttpService(); 
  5.        //耗時10ms 
  6.        Map<String, String>result1 = rpcService
    .getRpcResult(); 
  7.        //耗時20ms 
  8.        Integer result2 = httpService.getHttpResult(); 
  9.        //總耗時30ms 
  10.     } 
  11.    static class RpcService { 
  12.        Map<String, String> getRpcResult() throws Exception { 
  13.            //呼叫遠端方法(遠端方法耗時約10ms,可以使用Thread.sleep模擬) 
  14.        } 
  15.     } 
  16.    static class HttpService { 
  17.        Integer getHttpResult() throws Exception { 
  18.            //呼叫遠端方法(遠端方法耗時約20ms,可以使用Thread.sleep模擬) 
  19.            Thread.sleep(20); 
  20.            return 0; 
  21.        } 
  22.     } 

半非同步(非同步Future)

  1. public class Test { 
  2.    final static ExecutorService executor = Executors.newFixedThreadPool(2); 
  3.    public static void main(String[] args) { 
  4.        RpcService rpcService = new RpcService(); 
  5.        HttpService httpService = new HttpService(); 
  6.        Future<Map<String, String>>future1 = null
  7.        Future<Integer>future2 = null
  8.        try { 
  9.            future1 = executor.submit(() -> rpcService.getRpcResult()); 
  10.            future2 = executor.submit(() -> httpService.getHttpResult()); 
  11.            //耗時10ms 
  12.            Map<String, String>result1 = future1.get(300, TimeUnit.MILLISECONDS); 
  13.            //耗時20ms 
  14.            Integer result2 = future2.get(300, TimeUnit.MILLISECONDS); 
  15.            //總耗時20ms 
  16.        } catch (Exception e) { 
  17.            if (future1 != null) { 
  18.                 future1.cancel(true); 
  19.            } 
  20.            if (future2 != null) { 
  21.                 future2.cancel(true); 
  22.            } 
  23.            throw new RuntimeException(e); 
  24.        } 
  25.     } 
  26.    static class RpcService { 
  27.        Map<String, String> getRpcResult() throws Exception { 
  28.            //呼叫遠端方法(遠端方法耗時約10ms,可以使用Thread.sleep模擬) 
  29.        } 
  30.     } 
  31.    static class HttpService { 
  32.        Integer getHttpResult() throws Exception { 
  33.            //呼叫遠端方法(遠端方法耗時約20ms,可以使用Thread.sleep模擬) 
  34.        } 
  35.     } 

全非同步(Callback)

  1. public class AsyncTest { 
  2. public staticHttpAsyncClient httpAsyncClient; 
  3.    public static CompletableFuture<String> getHttpData(String url) { 
  4.        CompletableFuture asyncFuture = new CompletableFuture(); 
  5.        HttpPost post = new HttpPost(url); 
  6.        HttpAsyncRequestProducer producer = HttpAsyncMethods.create(post); 
  7.        AsyncCharConsumer<HttpResponse>consumer = newAsyncCharConsumer<HttpResponse>() { 
  8.             HttpResponse response; 
  9.            protected HttpResponse buildResult(final HttpContext context) { 
  10.                 return response; 
  11.            } 
  12. …... 
  13.        }; 
  14.        FutureCallback callback = new FutureCallback<HttpResponse>() { 
  15.            public void completed(HttpResponse response) { 
  16.                asyncFuture.complete(EntityUtils.toString(response.getEntity())); 
  17.            } 
  18. …... 
  19.        }; 
  20.        httpAsyncClient.execute(producer, consumer, callback); 
  21.        return asyncFuture; 
  22.     } 
  23.    public static void main(String[] args) throws Exception { 
  24.        AsyncTest.getHttpData("http://www.jd.com"); 
  25.        Thread.sleep(1000000); 
  26.     } 

本示例使用HttpAsyncClient演示。

非同步回撥鏈式編排

CompletableFuture提供了50多個API,可以滿足所需的各種場景的非同步處理的編排,在此列舉三個場景:

場景1:三個服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒;

方法test1:

  1. public static void test1() throws Exception { 
  2.       HelloClientDemoTest service = new HelloClientDemoTest(); 
  3.       /** 
  4.        * 場景1 兩個以上服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒 
  5.        * 並且兩個服務也是非同步非阻塞呼叫 
  6.        */ 
  7.       CompletableFuture future1 = service.getHttpData("http://www.jd.com"); 
  8.       CompletableFuture future2 = service.getHttpData("http://www.jd.com"); 
  9.       CompletableFuture future3 =service.getHttpData("http://www.jd.com"); 
  10.       List<CompletableFuture>futureList = Lists.newArrayList(future1,future2, future3); 
  11.       CompletableFuture<Void>allDoneFuture =CompletableFuture.allOf(futureList.toArray(newCompletableFuture[futureList.size()])); 
  12.       CompletableFuture<String>future4 =allDoneFuture.thenApply(v -> { 
  13.            List<Object>result =futureList.stream().map(CompletableFuture::join) 
  14.                   .collect(Collectors.toList()); 
  15.            //注意順序 
  16.            String result1 = (String)result.get(0); 
  17.            String result2 = (String)result.get(1); 
  18.            String result3 = (String)result.get(2); 
  19.            //處理業務.... 
  20.            return result1 + result2 + result3; 
  21.        }).exceptionally(e -> { 
  22.            //e.printStackTrace(); 
  23.            return ""; 
  24.        }); 
  25.       //返回 
  26.    } 

場景2、兩個服務併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒;

方法test2:

  1. public void test2() throws Exception { 
  2.       HelloClientDemoTest service = new HelloClientDemoTest(); 
  3.       /** 
  4.        * 場景2 兩個介面併發非同步呼叫,返回CompletableFuture,不阻塞主執行緒 
  5.        * 並且兩個服務也是非同步非阻塞呼叫 
  6.        */ 
  7.       CompletableFuture future1 = service.getHttpData("http://www.jd.com"); 
  8.       CompletableFuture future2 =service.getHttpData("http://www.jd.com"); 
  9.       CompletableFuture future3 =future1.thenCombine(future2, (f1, f2) -> { 
  10.            //處理業務.... 
  11.            return f1 + "," + f2; 
  12.        }).exceptionally(e -> { 
  13.            return ""; 
  14.        }); 
  15.       //返回 
  16.    } 

場景3、兩個服務,併發非同步呼叫兩個服務,並且一個服務的結果返回後再次呼叫另一服務,然後將三個結果後並處理,返回CompletableFuture,整個處理過程中不阻塞任何執行緒;

方法test3:

  1. publicvoid test3() throws Exception { 
  2.        HelloClientDemoTest service = new HelloClientDemoTest(); 
  3.        /** 
  4.         * 場景3 兩請求依賴呼叫,然後與另一服務結果組合處理,返回CompletableFuture,不阻塞主執行緒 
  5.         * 並且兩個服務也是非同步非阻塞呼叫 
  6.         */ 
  7.         CompletableFuture future1 = service.getHttpData("http://www.jd.com"); 
  8.         CompletableFuture future2 = service.getHttpData("http://www.jd.com"); 
  9.         CompletableFuture<String>future3future1.thenApply((param) -> { 
  10.             CompletableFuture future4 =service.getHttpData("http://www.jd.com"); 
  11.             return future4; 
  12.         }); 
  13.         CompletableFuture future5 =future2.thenCombine(future3, (f2, f3) -> { 
  14.             //....處理業務 
  15.             return f2 + "," + f3; 
  16.         }).exceptionally(e -> { 
  17.             return ""; 
  18.         }); 
  19.         //返回future5 
  20.     } 

全非同步Web系統設計

主要技術:servlet3,JDK8 CompletableFuture,支援非同步Callback呼叫的RPC框架。

先看一下處理流程圖:

servlet3:Servlet 接收到請求之後,可能首先需要對請求攜帶的資料進行一些預處理;接著,Servlet 執行緒將請求轉交給一個非同步執行緒來執行業務處理,執行緒本身返回至容器。針對業務處理較耗時的情況,這將大大減少伺服器資源的佔用,並且提高併發處理速度。servlet3可參考商品詳情頁系統的Servlet3非同步化實踐,結合其中講解的servlet3整合:

  1. public void submitFuture(finalHttpServletRequest req, final Callable<CompletableFuture> task) throwsException{ 
  2.        final String uri = req.getRequestURI(); 
  3.        final Map<String, String[]>params = req.getParameterMap(); 
  4.        final AsyncContext asyncContext = req.startAsync(); 
  5.        asyncContext.getRequest().setAttribute("uri", uri); 
  6.        asyncContext.getRequest().setAttribute("params", params); 
  7.        asyncContext.setTimeout(asyncTimeoutInSeconds * 1000); 
  8.        if(asyncListener != null) { 
  9.            asyncContext.addListener(asyncListener); 
  10.        } 
  11.        CompletableFuture future = task.call(); 
  12.        future.thenAccept(result -> { 
  13.            HttpServletResponse resp = (HttpServletResponse)asyncContext.getResponse(); 
  14.            try { 
  15.                 if(result instanceof String) { 
  16.                     byte[] bytes = new byte[0]; 
  17.                     if (StringUtils.isBlank(result)){ 
  18.                        resp.setContentType("text/html;charset=gbk"); 
  19.                        resp.setContentLength(0); 
  20.                     } else { 
  21.                         bytes =result.getBytes("GBK"); 
  22.                     } 
  23.                    //resp.setBufferSize(bytes.length); 
  24.                    resp.setContentType("text/html;charset=gbk"); 
  25.                    if(StringUtils.isNotBlank(localIp)) { 
  26.                        resp.setHeader("t.ser", localIp); 
  27.                     } 
  28.                    resp.setContentLength(bytes.length); 
  29.                    resp.getOutputStream().write(bytes); 
  30.                 } else { 
  31.                     write(resp,JSONUtils.toJSON(result)); 
  32.                 } 
  33.            } catch (Throwable e) { 
  34.                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); //程式內部錯誤 
  35.                 try { 
  36.                     LOG.error("get infoerror, uri : {},  params : {}", uri,JSONUtils.toJSON(params), e); 
  37.                 } catch (Exception ex) { 
  38.                 } 
  39.            } finally { 
  40.                 asyncContext.complete(); 
  41.            } 
  42.        }).exceptionally(e -> { 
  43.            asyncContext.complete(); 
  44.            return null; 
  45.        }); 

另外還有Java中協程庫Quasar,可參考《Java的纖程庫 - Quasar》,目前沒有在應用中使用並在測試FiberHttpServlet的時候遇到很多坑,日後把Quasar自如運用後形成日記,希望能結實更多的朋友一起研究,踩坑。

作者:孫偉,目前負責京東商品詳情頁統一服務系統,寫過java,寫過ngx_lua,還寫過storm等,喜歡學習研究新事物。

【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的部落格),公眾號id: kaitao-1234567】