點選上方“芋道原始碼”,選擇“置頂公眾號”
技術文章第一時間送達!
原始碼精品專欄
-
為什麼慢?
-
多執行緒加速
-
非同步呼叫
-
RPC 非同步呼叫
-
總結
這週六參加了一個美團點評的技術沙龍,其中一位老師在介紹他們自研的 RPC 框架時提到一點:RPC 請求分為 sync,future,callback,oneway,並且需要遵循一個原則:能夠非同步的地方就不要使用同步。正好最近在最佳化一個業務場景:在一次頁面展示中,需要呼叫 5 個 RPC 介面,導致頁面響應很慢。正好啟發了我。
為什麼慢?
大多數開源的 RPC 框架實現遠端呼叫的方式都是同步的,假設 [ 介面1,…,介面5]的每一次呼叫耗時為 200ms (其中介面2依賴介面1,介面5依賴介面3,介面4),那麼總耗時為 1s,這整個是一個序列的過程。
多執行緒加速
第一個想到的解決方案便是多執行緒,那麼[1=>2]編為一組,[[3,4]=>5]編為一組,兩組併發執行,[1=>2]序列執行耗時400ms,[3,4]併發執行耗時200ms,[[3,4]=>5]總耗時400ms ,最終[[1=>2],[[3,4]=>5]]總耗時400ms(理論耗時)。相比較於原來的1s,的確快了不少,但實際編寫介面花了不少功夫,建立執行緒池,管理資源,分析依賴關係…總之程式碼不是很優雅。
RPC中,多執行緒著重考慮的點是在客戶端最佳化程式碼,這給客戶端帶來了一定的複雜性,並且編寫併發程式碼對程式員的要求更高,且不利於除錯。
非同步呼叫
如果有一種既能保證速度,又能像同步 RPC 呼叫那樣方便,豈不美哉?於是引出了 RPC 中的非同步呼叫。
在 RPC 非同步呼叫之前,先回顧一下 java.util.concurrent
中的基礎知識:Callable
和 Future
public class Main {
public static void main(String[] args) throws Exception{
final ExecutorService executorService = Executors.newFixedThreadPool(10);
long start = System.currentTimeMillis();
Future resultFuture1 = executorService.submit(new Callable() {
@Override
public Integer call() throws Exception {
return method1() + method2();
}
});
Future resultFuture2 = executorService.submit(new Callable() {
@Override
public Integer call() throws Exception {
Future resultFuture3 = executorService.submit(new Callable() {
@Override
public Integer call() throws Exception {
return method3();
}
});
Future resultFuture4 = executorService.submit(new Callable() {
@Override
public Integer call() throws Exception {
return method4();
}
});
return method5()+resultFuture3.get()+resultFuture4.get();
}
});
int result = resultFuture1.get() + resultFuture2.get();
System.out.println("result = "+result+", total cost "+(System.currentTimeMillis()-start)+" ms");
executorService.shutdown();
}
static int method1(){
delay200ms();
return 1;
}
static int method2(){
delay200ms();
return 2;
}
static int method3(){
delay200ms();
return 3;
}
static int method4(){
delay200ms();
return 4;
}
static int method5(){
delay200ms();
return 5;
}
static void delay200ms(){
try{
Thread.sleep(200);
}catch (Exception e){
e.printStackTrace();
}
}
}
最終控制檯列印:
result = 15, total cost 413 ms
五個介面,如果同步呼叫,便是序列的效果,最終耗時必定在 1s 之上,而非同步呼叫的優勢便是,submit任務之後立刻傳回,只有在呼叫 future.get()
方法時才會阻塞,而這期間多個非同步方法便可以併發的執行。
RPC 非同步呼叫
我們的專案使用了 Motan 作為 RPC 框架,檢視其 changeLog ,0.3.0 (2017-03-09) 該版本已經支援了 async 特性。可以讓開發者很方便地實現 RPC 非同步呼叫。
1 為介面增加 @MotanAsync 註解
@MotanAsync
public interface DemoApi {
DemoDto randomDemo(String id);
}
2 新增 Maven 外掛
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojogroupId>
<artifactId>build-helper-maven-pluginartifactId>
<version>1.10version>
<executions>
<execution>
<phase>generate-sourcesphase>
<goals>
<goal>add-sourcegoal>
goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/annotationssource>
sources>
configuration>
execution>
executions>
plugin>
plugins>
build>
安裝外掛後,可以藉助它生成一個和 DemoApi 關聯的非同步介面 DemoApiAsync 。
public interface DemoApiAsync extends DemoApi {
ResponseFuture randomDemoAsync(String id);
}
3 註入介面即可呼叫
@Service
public class DemoService {
@MotanReferer
DemoApi demoApi;
@MotanReferer
DemoApiAsync demoApiAsync;//<1>
public DemoDto randomDemo(String id){
DemoDto demoDto = demoApi.randomDemo(id);
return demoDto;
}
public DemoDto randomDemoAsync(String id){
ResponseFuture responseFuture = demoApiAsync.randomDemoAsync(id);//<2>
DemoDto demoDto = (DemoDto) responseFuture.getValue();
return demoDto;
}
}
<1> DemoApiAsync 如何生成的已經介紹過,它和 DemoApi 並沒有功能性的區別,僅僅是同步非同步呼叫的差距,而 DemoApiAsync 實現的的複雜性完全由 RPC 框架幫助我們完成,開發者無需編寫 Callable 介面。
<2> ResponseFuture 是 RPC 中 Future 的抽象,其本身也是 juc 中 Future 的子類,當 responseFuture.getValue() 呼叫時會阻塞。
總結
在非同步呼叫中,如果發起一次非同步呼叫後,立刻使用 future.get() ,則大致和同步呼叫等同。其真正的優勢是在submit 和 future.get() 之間可以混雜一些非依賴性的耗時操作,而不是同步等待,從而充分利用時間片。
另外需要註意,如果非同步呼叫涉及到資料的修改,則多個非同步操作直接不能保證 happens-before 原則,這屬於併發控制的範疇了,謹慎使用。查詢操作則大多沒有這樣的限制。
在能使用併發的地方使用併發,不能使用的地方才選擇同步,這需要我們思考更多細節,但可以最大限度的提升系統的效能。
666. 彩蛋
如果你對 RPC 併發感興趣,歡迎加入我的知識一起交流。
目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:
01. 除錯環境搭建
02. 專案結構一覽
03. API 配置(一)之應用
04. API 配置(二)之服務提供者
05. API 配置(三)之服務消費者
06. 屬性配置
07. XML 配置
08. 核心流程一覽
…
一共 60 篇++