性能優化-技術專題-併發編程

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"系統性能優化之併發編程"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一.什麼是高併發"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 高併發(High Concurrency)是互聯網分佈式系統架構設計中必須考慮的因素之一。它通常是指,通過設計保證系統能夠同時並行處理很多請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":" 高併發指標通常有四點:響應時間(RT)、吞吐量(Throughput)、QPS(Queries-per-second)、併發用戶數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":" 注:一個用戶一秒內狂點按鈕10次,這不是併發。只有10次請求在同一時刻同時向服務器發送請求(比如用CountDownLatch模擬實現),這纔是併發。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 響應時間(RT)"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"響應時間是指系統對請求作出響應的時間,一般來講,用戶能接受的響應時間是小於5秒。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 吞吐量(Throughput)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"吞吐量是指系統在單位時間內處理請求的數量。對於無併發的應用系統而言,吞吐量與響應時間成嚴格的反比關係,實際上此時吞吐量就是響應時間的倒數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"一個系統的吞度量(承壓能力)與"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"request對CPU的消耗、外部接口、IO等等緊密關聯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"系統吞吐量幾個重要參數:"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"QPS(TPS)、併發數、響應時間"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"QPS(TPS):每秒鐘request/事務數量"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"併發數: 系統同時處理的request/事務數量"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"響應時間: 一般取平均響應時間"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理解了上面三個要素的意義之後,就能推算出它們之間的關係:"}]},{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"QPS(TPS)= 併發數/平均響應時間"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 一個系統吞吐量通常由QPS(TPS)、併發數兩個因素決定,每套系統這兩個值都有一個相對極限值,在應用場景訪問壓力下,只要某一項達到系統最高值,系統的吞吐量就上不去了,如果壓力繼續增大,系統的吞吐量反而會下降,原因是系統超負荷工作,上下文切換、內存等等其它消耗導致系統性能下降。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.3 QPS(Queries-per-second)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每秒響應請求數"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.4 併發用戶數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"併發用戶數是指系統可以同時承載的正常使用系統功能的用戶的數量。與吞吐量相比,併發用戶數是一個更直觀但也更籠統的性能指標。實際上,併發用戶數是一個非常不準確的指標,因爲用戶不同的使用模式會導致不同用戶在單位時間發出不同數量的請求。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"二、高併發帶來的問題"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.1 服務端"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"某一時間片刻系統流量異常高,系統瀕臨閥值"},{"type":"text","text":";"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"服務器CPU,內存爆滿,磁盤IO繁忙"},{"type":"text","text":";"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"系統雪崩:分佈式系統中經常會出現某個基礎服務不可用造成整個系統不可用的情況,這種現象被稱爲服務雪崩效應。"},{"type":"text","text":"服務雪崩效應是一種因服務提供者的不可用導致服務調用者的不可用,並將不可用逐漸放大的過程。A爲服務提供者,B爲A的服務調用者,C和D是B的服務調用者。當A的不可用,引起B的不可用,並將不可用逐漸放大C和D時,服務雪崩就形成了。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.2 用戶角度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":"使用體驗差"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"三、併發解決方案-四大法定"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"緩存:Redis"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"異步:消息中間件MQ"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"併發編程"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"分佈式"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"四、優化方案——併發編程"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.1阿里筆試題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"支付寶登錄後,請問你可以從哪些角度優化提升性能?業務場景架構圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c100de770045e7bee4b3704c5c714f22.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6b/6b0aedec07df6a644380ff51e2bf6140.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.2 模擬業務場景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 採用SpringBoot創建兩個項目,一個項目(remote-server)爲模擬遠程服務接口項目,提供用戶基本信息、用戶餘額查詢等API。另外一個項目(app-server)調用遠程服務接口,然後用fastjson組裝數據後向APP端提供統一的用戶信息API。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2.1 remote-server項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、端口設置8081"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"server:\n port: 8081\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、導入我們需要用到的fastjson"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \n com.alibaba\n fastjson\n 1.2.62\n "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、創建兩個實體類,分別爲:用戶基本信息"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class UserBaseInfo {\n private String userId;\n private String userName;\n private String sex;\n private int age;\n // get set 省略\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶餘額"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class UserMoney {\n private String userId;\n private String money;\n // get set 省略\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、分別提供兩個API供遠程調用,分別爲:用戶基本信息API"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@RestController\n@RequestMapping(\"users-base\")\npublic class UserBaseInfoController {\n @GetMapping(\"/{userId}\")\n public UserBaseInfo getUserBaseInfo(@PathVariable String userId){\n System.out.println(\"userId:\"+userId);\n try {\n // 模擬業務邏輯,等待2秒\n Thread.sleep(2000);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n UserBaseInfo userBaseInfo = new UserBaseInfo();\n userBaseInfo.setUserId(\"1\");\n userBaseInfo.setUserName(\"AlanChen\");\n userBaseInfo.setSex(\"男\");\n userBaseInfo.setAge(18);\n return userBaseInfo;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶餘額API"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@RestController\n@RequestMapping(\"users-money\")\npublic class UserMoneyController {\n\n @GetMapping(\"/{userId}\")\n public UserMoney getUserMoney(@PathVariable String userId){\n System.out.println(\"userId:\"+userId);\n try {\n // 模擬業務邏輯,等待2秒\n Thread.sleep(2000);\n } catch (InterruptedException e) {\n e.printStackTrace();\n }\n UserMoney userMoney = new UserMoney();\n userMoney.setUserId(\"1\");\n userMoney.setMoney(\"1000\");\n return userMoney;\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、用Postman測試接口如下"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e1/e1191b4b52a032f57a16822e992dc503.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"用戶基本信息"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/96/96e120688330209d981df8cd4c2b139c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"用戶餘額"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"4.2.2 app-server項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、端口設置爲8888"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"server:\n port: 8888\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、導入我們需要用到的fastjson"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" \n com.alibaba\n fastjson\n 1.2.62\n \n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、用Spring的RestTemplate接口實現遠程調用服務"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * @author Alan Chen\n * @description 遠程接口\n * @date 2020-07-20\n */\n@Service\npublic class RemoteService {\n\n /**\n * 獲取用戶基本信息\n * @param userId\n * @return\n */\n public String getUserInfo(String userId){\n RestTemplate restTemplate = new RestTemplate();\n long t1 = System.currentTimeMillis();\n String result = restTemplate.getForObject(\"http://127.0.0.1:8081/users-base/{userId}\",String.class,userId);\n\n System.out.println(\"獲取用戶基本信息時間爲:\"+(System.currentTimeMillis()-t1));\n return result;\n }\n\n /**\n * 獲取用戶餘額\n * @param userId\n * @return\n */\n public String getUserMoney(String userId){\n RestTemplate restTemplate = new RestTemplate();\n long t1 = System.currentTimeMillis();\n String result = restTemplate.getForObject(\"http://127.0.0.1:8081/users-money/{userId}\",String.class,userId);\n\n System.out.println(\"獲取用戶餘額時間爲:\"+(System.currentTimeMillis()-t1));\n return result;\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、調用遠程接口,數據組裝後返回給客戶端"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@Service\npublic class UserService {\n\n @Autowired\n RemoteService remoteService;\n\n ExecutorService task = Executors.newFixedThreadPool(10);\n\n /**\n * 串行調用遠程接口\n * @param userId\n * @return\n */\n public String getUserInfo(String userId){\n\n long t1 = System.currentTimeMillis();\n\n // 分別調用兩個接口\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n\n // 結果集進行合併\n JSONObject result = new JSONObject();\n result.putAll(userInfo);\n result.putAll(moneyInfo);\n\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n\n return result.toString();\n }\n\n /**\n * 線程並行調用遠程接口(Thread+FutureTask+Callable)\n * @param userId\n * @return\n */\n public String getUserInfoThread(String userId){\n // Runnable沒有返回值,Callable有返回值\n\n long t1 = System.currentTimeMillis();\n\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n\n FutureTask queryUserInfoFutureTask = new FutureTask<>(queryUserInfo);\n FutureTask queryUserMoneyFutureTask = new FutureTask<>(queryUserMoney);\n\n new Thread(queryUserInfoFutureTask).start();\n new Thread(queryUserMoneyFutureTask).start();\n\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n return result.toString();\n }\n\n /**\n * 線程並行調用遠程接口(Thread+Callable+自定義FutureTask)\n * @param userId\n * @return\n */\n public String getUserInfoThreadMyFutureTask(String userId){\n // Runnable沒有返回值,Callable有返回值\n long t1 = System.currentTimeMillis();\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n AlanChenFutureTask queryUserInfoFutureTask = new AlanChenFutureTask<>(queryUserInfo);\n AlanChenFutureTask queryUserMoneyFutureTask = new AlanChenFutureTask<>(queryUserMoney);\n new Thread(queryUserInfoFutureTask).start();\n new Thread(queryUserMoneyFutureTask).start();\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n return result.toString();\n }\n\n /**\n * 線程並行調用遠程接口(Thread+FutureTask+Callable+ExecutorService)\n * @param userId\n * @return\n */\n public String getUserInfoThreadPool(String userId){\n // Runnable沒有返回值,Callable有返回值\n\n long t1 = System.currentTimeMillis();\n\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n\n FutureTask queryUserInfoFutureTask = new FutureTask<>(queryUserInfo);\n FutureTask queryUserMoneyFutureTask = new FutureTask<>(queryUserMoney);\n\n //用線程池執行\n task.submit(queryUserInfoFutureTask);\n task.submit(queryUserMoneyFutureTask);\n\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n\n return result.toString();\n }\n\n /**\n * 異步請求(節省tomcat線程池線程) Callable或DeferredResult\n * @param userId\n * @return\n */\n public Callable getUserInfoAsync(@PathVariable String userId){\n long t = System.currentTimeMillis();\n System.out.println(\"主線程開始...\"+Thread.currentThread());\n\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n long t1 = System.currentTimeMillis();\n System.out.println(\"子線程開始...\"+Thread.currentThread());\n\n String result = getUserInfoThreadPool(userId);\n\n System.out.println(\"子線程結束...\"+Thread.currentThread()+\"---->\"+(System.currentTimeMillis()-t1));\n return result;\n }\n };\n\n System.out.println(\"主線程結束...\"+Thread.currentThread()+\"---->\"+(System.currentTimeMillis()-t));\n return callable;\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、提供Controller接口給客戶端訪問"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@RestController\n@RequestMapping(\"users\")\npublic class UserController {\n\n @Autowired\n UserService userService;\n\n @GetMapping(\"/{userId}\")\n public String getUserInfo(@PathVariable String userId){\n return userService.getUserInfo(userId);\n }\n\n @GetMapping(\"/thread/{userId}\")\n public String getUserInfoThread(@PathVariable String userId){\n return userService.getUserInfoThread(userId);\n }\n\n @GetMapping(\"/thread/pool/{userId}\")\n public String getUserInfoThreadPool(@PathVariable String userId){\n return userService.getUserInfoThreadPool(userId);\n }\n\n @GetMapping(\"/thread/my_future_task/{userId}\")\n public String getUserInfoThreadMyFutureTask(@PathVariable String userId){\n return userService.getUserInfoThreadMyFutureTask(userId);\n }\n\n @GetMapping(\"/thread/async/{userId}\")\n public Callable getUserInfoAsync(@PathVariable String userId){\n return userService.getUserInfoAsync(userId);\n }\n\n}\n\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.3 優化方案詳解"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有的優化方案都在app-server的UserService實現類裏"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3.1 常規實現方式:串行調用遠程接口"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" /**\n * 串行調用遠程接口\n * @param userId\n * @return\n */\n public String getUserInfo(String userId){\n\n long t1 = System.currentTimeMillis();\n\n // 分別調用兩個接口\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n\n // 結果集進行合併\n JSONObject result = new JSONObject();\n result.putAll(userInfo);\n result.putAll(moneyInfo);\n\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n\n return result.toString();\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行結果"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8e/8e6d71654038d974f124eeef1dbfbf79.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"postman執行結果"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/06/0679ad052d2411a12323f20cbf2dc96a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制檯打印執行時間"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我看可以看到,執行時間大約是4秒鐘。這種串行調用遠程接口的方式,時間執行爲各遠程接口調用的時間總和,接口響應慢,不可取。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3.2 線程並行調用遠程接口:Thread+FutureTask+Callable"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * 線程並行調用遠程接口(Thread+FutureTask+Callable)\n * @param userId\n * @return\n */\n public String getUserInfoThread(String userId){\n // Runnable沒有返回值,Callable有返回值\n long t1 = System.currentTimeMillis();\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n FutureTask queryUserInfoFutureTask = new FutureTask<>(queryUserInfo);\n FutureTask queryUserMoneyFutureTask = new FutureTask<>(queryUserMoney);\n new Thread(queryUserInfoFutureTask).start();\n new Thread(queryUserMoneyFutureTask).start();\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n return result.toString();\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/50/500b4dd27bd5bc0f55fb49f5e653765d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"控制檯打印執行時間"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我看可以看到,執行時間大約是2秒鐘,執行時間減少了一半。利用Thread+FutureTask+Callable,可以同時發送多個遠程調用請求,再用FutureTask的get()方法阻塞拿到各異步請求的結果集,再進行合併。這種方案,接口執行的總時間取決於各異步遠程接口調用的最長的那個時間。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f8/f8c6592394785b116e35d05e12cfbcf5.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"線程並行調用接口"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3.3 線程並行調用遠程接口:Thread+Callable+自定義FutureTask"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FutureTask除了用JDK自帶的接口外,我們自己同樣也可以實現一個簡單的FutureTask。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * @author Alan Chen\n * @description 自定義FutureTask\n * @date 2020-07-20\n */\npublic class AlanChenFutureTask implements Runnable, Future {\n\n Callable callable;\n V result;\n\n public AlanChenFutureTask(Callable callable){\n this.callable = callable;\n }\n\n @Override\n public void run() {\n try {\n result = callable.call();\n synchronized (this){\n this.notifyAll();\n }\n } catch (Exception e) {\n e.printStackTrace();\n }\n }\n\n @Override\n public V get() throws InterruptedException, ExecutionException {\n if(result!=null){\n return result;\n }\n synchronized (this){\n //阻塞等待獲取返回值\n this.wait();\n }\n return result;\n }\n\n @Override\n public boolean cancel(boolean mayInterruptIfRunning) {\n return false;\n }\n\n @Override\n public boolean isCancelled() {\n return false;\n }\n\n @Override\n public boolean isDone() {\n return false;\n }\n\n @Override\n public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {\n return null;\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * 線程並行調用遠程接口(Thread+Callable+自定義FutureTask)\n * @param userId\n * @return\n */\n public String getUserInfoThreadMyFutureTask(String userId){\n // Runnable沒有返回值,Callable有返回值\n long t1 = System.currentTimeMillis();\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n AlanChenFutureTask queryUserInfoFutureTask = new AlanChenFutureTask<>(queryUserInfo);\n AlanChenFutureTask queryUserMoneyFutureTask = new AlanChenFutureTask<>(queryUserMoney);\n new Thread(queryUserInfoFutureTask).start();\n new Thread(queryUserMoneyFutureTask).start();\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n return result.toString();\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8f/8f143cc3f3ead94ec721887b6a9602bc.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制檯打印執行時間"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3.4 線程池並行調用遠程接口:Thread+FutureTask+Callable+ExecutorService"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以進一步進行優化,將線程換成線程池"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * 線程池並行調用遠程接口(Thread+FutureTask+Callable+ExecutorService)\n * @param userId\n * @return\n */\n public String getUserInfoThreadPool(String userId){\n // Runnable沒有返回值,Callable有返回值\n\n long t1 = System.currentTimeMillis();\n\n Callable queryUserInfo = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v1 = remoteService.getUserInfo(userId);\n JSONObject userInfo = JSONObject.parseObject(v1);\n return userInfo;\n }\n };\n\n Callable queryUserMoney = new Callable() {\n @Override\n public JSONObject call() throws Exception {\n String v2 = remoteService.getUserMoney(userId);\n JSONObject moneyInfo = JSONObject.parseObject(v2);\n return moneyInfo;\n }\n };\n\n FutureTask queryUserInfoFutureTask = new FutureTask<>(queryUserInfo);\n FutureTask queryUserMoneyFutureTask = new FutureTask<>(queryUserMoney);\n\n //用線程池執行\n task.submit(queryUserInfoFutureTask);\n task.submit(queryUserMoneyFutureTask);\n\n // 結果集進行合併\n JSONObject result = new JSONObject();\n try {\n result.putAll(queryUserInfoFutureTask.get());// 阻塞方法拿結果\n result.putAll(queryUserMoneyFutureTask.get());// 阻塞方法拿結果\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n System.out.println(\"執行總時間爲:\"+(System.currentTimeMillis()-t1));\n\n return result.toString();\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7d/7d0e267cb049f812a2beb41dcf6372c4.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制檯打印執行時間"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3.5 異步請求(節省tomcat線程池線程) :Callable或DeferredResult"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 我們知道tomcat的請求連接數是有限的,如果接口響應時間過長,會佔用和消耗tomcat的連接數。如果tomcat主線程接收到請求後,立即開啓一個子線程異步去執行業務邏輯,然後tomcat主線程快速返回釋放連接,等有結果後子線程再返回給前端客戶端,這樣就可以大大節省tomcat的連接數,提高tomcat連接利用率。具體實現如下:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/81/81b4724f4f86e9ac0a8d923c94ddc2d4.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"異步請求架構圖"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * 異步請求(節省tomcat線程池線程) Callable或DeferredResult\n * @param userId\n * @return\n */\n public Callable getUserInfoAsync(@PathVariable String userId){\n long t = System.currentTimeMillis();\n System.out.println(\"主線程開始...\"+Thread.currentThread());\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n long t1 = System.currentTimeMillis();\n System.out.println(\"子線程開始...\"+Thread.currentThread());\n String result = getUserInfoThreadPool(userId);\n System.out.println(\"子線程結束...\"+Thread.currentThread()+\"---->\"+(System.currentTimeMillis()-t1));\n return result;\n }\n };\n System.out.println(\"主線程結束...\"+Thread.currentThread()+\"---->\"+(System.currentTimeMillis()-t));\n return callable;\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/14/1471dbf21eb18dbcdbf050686bae470d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"postman執行測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d4/d4980928f339dfba8852107f0b37b4b0.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制檯打印結果"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們從打印結果中可以看到,tomcat主線程幾乎只用了0毫秒就快速返回了,等子線程取到結果後再返回給客戶端,消耗時間大約爲2秒。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章