對於開發者來講,異步是一種程序設計的思想,使用異步模式設計的程序能夠顯著減小線程等待,從而在高吞吐量的場景中,極大提高系統的總體性能,顯著下降時延。git
所以,像消息隊列這種須要超高吞吐量和超低時延的中間件系統,在其核心流程中,必定會大量採用異步的設計思想。程序員
接下來,咱們一塊兒來經過一個很是簡單的例子學習一下,使用異步設計是如何提高系統性能的。github
假設咱們要實現一個轉帳的微服務 Transfer( accountFrom, accountTo, amount),這個服務有三個參數:分別是轉出帳戶、轉入帳戶和轉帳金額。面試
實現過程也比較簡單,咱們要從帳戶 A 中轉帳 100 元到帳戶 B 中:編程
對應的時序圖是這樣的:性能優化
在這個例子的實現過程當中,咱們調用了另一個微服務 Add(account, amount),它的功能是給帳戶 account 增長金額 amount,當 amount 爲負值的時候,就是扣減響應的金額。服務器
須要特別說明的是,在這段代碼中,我爲了使問題簡化以便咱們能專一於異步和性能優化,省略了錯誤處理和事務相關的代碼,你在實際的開發中不要這樣作。網絡
首先咱們來看一下同步實現,對應的僞代碼以下:框架
Transfer(accountFrom, accountTo, amount) { //先從accountFrom的帳戶中減去相應的錢數 Add(accountFrom, -1 * amount) //再把減去的錢數加到accountTo的帳戶中 Add(accountTo, amount) return OK }
上面的僞代碼首先從 accountFrom 的帳戶中減去相應的錢數,再把減去的錢數加到accountTo 的帳戶中,這種同步實現是一種很天然方式,簡單直接。那麼性能表現如何呢?接下來咱們就來一塊兒分析一下性能。異步
假設微服務 Add 的平均響應時延是 50ms,那麼很容易計算出咱們實現的微服務 Transfer的平均響應時延大約等於執行 2 次 Add 的時延,也就是 100ms。那隨着調用 Transfer 服務的請求愈來愈多,會出現什麼狀況呢?
在這種實現中,每處理一個請求須要耗時 100ms,並在這 100ms 過程當中是須要獨佔一個線程的,那麼能夠得出這樣一個結論:每一個線程每秒鐘最多能夠處理 10 個請求。咱們知道,每臺計算機上的線程資源並非無限的,假設咱們使用的服務器同時打開的線程數量上限是 10,000,能夠計算出這臺服務器每秒鐘能夠處理的請求上限是: 10,000 (個線程)*10(次請求每秒) = 100,000 次每秒。
若是請求速度超過這個值,那麼請求就不能被立刻處理,只能阻塞或者排隊,這時候Transfer 服務的響應時延由 100ms 延長到了:排隊的等待時延 + 處理時延 (100ms)。也就是說,在大量請求的狀況下,咱們的微服務的平均響應時延變長了。
這是否是已經到了這臺服務器所能承受的極限了呢?其實遠遠沒有,若是咱們監測一下服務器的各項指標,會發現不管是 CPU、內存,仍是網卡流量或者是磁盤的 IO 都空閒的很,那咱們 Transfer 服務中的那 10,000 個線程在幹什麼呢?對,絕大部分線程都在等待 Add服務返回結果。
也就是說,採用同步實現的方式,整個服務器的全部線程大部分時間都沒有在工做,而是都在等待。
若是咱們能減小或者避免這種無心義的等待,就能夠大幅提高服務的吞吐能力,從而提高服務的整體性能。
接下來咱們看一下,如何用異步的思想來解決這個問題,實現一樣的業務邏輯。
TransferAsync(accountFrom, accountTo, amount, OnComplete()) { //異步從accountFrom的帳戶中減去相應的錢數,而後調用OnDebit方法。 AddAsync(accountFrom, -1 * amount, OnDebit(accountTo, amount, OnAllDone(OnComplete()) } //扣減帳戶accountFrom完成後調用 OnDebit(accountTo, amount, OnAllDone(OnComplete())) { //再異步把減去的錢數加到accountTo的帳戶中,而後執行OnAllDone方法 AddAsync(accountTo, amount, OnAllDone(OnComplete())) } //轉入帳戶accountTo完成後調用 OnAllDone(OnComplete()) { OnComplete() }
細心的你可能已經注意到了,TransferAsync 服務比 Transfer 多了一個參數,而且這個參數傳入的是一個回調方法 OnComplete()(雖然 Java 語言並不支持將方法做爲方法參數傳遞,但像 JavaScript 等不少語言都具備這樣的特性,在 Java 語言中,也能夠經過傳入一個回調類的實例來變相實現相似的功能)。
這個 TransferAsync() 方法的語義是:請幫我執行轉帳操做,當轉帳完成後,請調用OnComplete() 方法。調用 TransferAsync 的線程沒必要等待轉帳完成就能夠當即返回了,待轉帳結束後,TransferService 天然會調用 OnComplete() 方法來執行轉帳後續的工做。
異步的實現過程相對於同步來講,稍微有些複雜。咱們先定義 2 個回調方法:
整個異步實現的語義至關於:
繪製成時序圖是這樣的:
你會發現,異步化實現後,整個流程的時序和同步實現是徹底同樣的,區別只是在線程模型上由同步順序調用改成了異步調用和回調的機制。
接下來咱們分析一下異步實現的性能,因爲流程的時序和同步實現是同樣,在低請求數量的場景下,平均響應時延同樣是 100ms。在超高請求數量場景下,異步的實現再也不須要線程等待執行結果,只須要個位數量的線程,便可實現同步場景大量線程同樣的吞吐量。
因爲沒有了線程的數量的限制,整體吞吐量上限會大大超過同步實現,而且在服務器CPU、網絡帶寬資源達到極限以前,響應時延不會隨着請求數量增長而顯著升高,幾乎能夠一直保持約 100ms 的平均響應時延。
看,這就是異步的魔力。
在實際開發時,咱們可使用異步框架和響應式框架,來解決一些通用的異步編程問題,簡化開發。Java 中比較經常使用的異步框架有 Java8 內置的CompletableFuture和 ReactiveX 的RxJava,我我的比較喜歡簡單實用易於理解的 CompletableFuture,可是 RxJava 的功能更增強大。有興趣的同窗能夠深刻了解一下。
Java 8 中新增了一個很是強大的用於異步編程的類:CompletableFuture,幾乎囊獲了咱們在開發異步程序的大部分功能,使用 CompletableFuture 很容易編寫出優雅且易於維護的異步代碼。
接下來,咱們來看下,如何用 CompletableFuture 實現的轉帳服務。
首先,咱們用 CompletableFuture 定義 2 個微服務的接口:
/** *帳戶服務 */ public interface AccountService { /** *變動帳戶金額 * @param account帳戶ID * @param amount增長的金額,負值爲減小 */ CompletableFuture<Void> add(int account, int amount); }
/** *轉帳服務 */ public interface TransferService { /** *異步轉帳服務 * @param fromAccount轉出帳戶 * @param toAccount轉入帳戶 * @param amount轉帳金額,單位分 */ CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount); }
能夠看到這兩個接口中定義的方法的返回類型都是一個帶泛型的 CompletableFeture,尖括號中的泛型類型就是真正方法須要返回數據的類型,咱們這兩個服務不須要返回數據,因此直接用 Void 類型就能夠。
而後咱們來實現轉帳服務:
/** *轉帳服務的實現 */ public class TransferServiceImpl implements TransferService { @Inject private AccountService accountService; //使用依賴注入獲取帳戶服務的實例 @Override public CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount) //異步調用add方法從fromAccount扣減相應金額 return accountService.add(fromAccount, -1 * amount) //而後調用add方法給toAccount增長相應金額 .thenCompose(v -> accountService.add(toAccount, amount)); } }
在轉帳服務的實現類 TransferServiceImpl 裏面,先定義一個 AccountService 實例,這個實例從外部注入進來,至於怎麼注入不是咱們關心的問題,就假設這個實例是可用的就行了。
而後咱們看實現 transfer() 方法的實現,咱們先調用一次帳戶服務 accountService.add()方法從 fromAccount 扣減響應的金額,由於 add() 方法返回的就是一個CompletableFeture 對象,能夠用 CompletableFeture 的 thenCompose() 方法將下一次調用 accountService.add() 串聯起來,實現異步依次調用兩次帳戶服務完整轉帳。
客戶端使用 CompletableFuture 也很是靈活,既能夠同步調用,也能夠異步調用。
public class Client { @Inject private TransferService transferService; //使用依賴注入獲取轉帳服務的實例 private final static int A = 1000; private final static int B = 1001; public void syncInvoke() throws ExecutionException, InterruptedException { //同步調用 transferService.transfer(A, B, 100).get(); System.out.println("轉帳完成!"); } public void asyncInvoke() { //異步調用 transferService.transfer(A, B, 100) .thenRun(() -> System.out.println("轉帳完成!")); } }
在調用異步方法得到返回值 CompletableFuture 對象後,既能夠調用CompletableFuture 的 get 方法,像調用同步方法那樣等待調用的方法執行結束並得到返回值,也能夠像異步回調的方式同樣,調用 CompletableFuture 那些以 then 開頭的一系列方法,爲 CompletableFuture 定義異步方法結束以後的後續操做。好比像上面這個例子中,咱們調用 thenRun() 方法,參數就是將轉帳完成打印在控臺上這個操做,這樣就能夠實如今轉帳完成後,在控制檯打印「轉帳完成!」了。
簡單的說,異步思想就是,當咱們要執行一項比較耗時的操做時,不去等待操做結束,而是給這個操做一個命令:「當操做完成後,接下來去執行什麼。」
使用異步編程模型,雖然並不能加快程序自己的速度,但能夠減小或者避免線程等待,只用不多的線程就能夠達到超高的吞吐能力。
同時咱們也須要注意到異步模型的問題:相比於同步實現,異步實現的複雜度要大不少,代碼的可讀性和可維護性都會顯著的降低。雖然使用一些異步編程框架會在必定程度上簡化異步開發,可是並不能解決異步模型高複雜度的問題。
異步性能雖好,但必定不要濫用,只有相似在像消息隊列這種業務邏輯簡單而且須要超高吞吐量的場景下,或者必須長時間等待資源的地方,才考慮使用異步模型。若是系統的業務邏輯比較複雜,在性能足夠知足業務需求的狀況下,採用符合人類天然的思路且易於開發和維護的同步模型是更加明智的選擇。