這篇文章主要描述非同步設計,非同步是一種程式設計的思想,使用非同步模式設計的程式可以顯著減少線程等待,從而在高吞吐量的場景中,極大提升系統的整體性能,降低請求時延。 ...
非同步是一種程式設計的思想,使用非同步模式設計的程式可以顯著減少線程等待,從而在高吞吐量的場景中,極大提升系統的整體性能,降低請求時延。
同步設計流程
我們假設要做一個轉賬的業務,即從賬戶A中轉賬100元到賬戶B中,它包含2步:
- 從A的賬戶中減少100元
- 給B的賬戶增加100元
我們可以設計2個Service:
- Transfer服務,負責轉賬,介面是Transfer(A, B, 100)
- Account服務,負責賬戶管理,介面是Add(A, -100)和Add(B, 100)
轉賬業務的偽代碼如下:
Transfer(accountFrom, accountTo, amount){
Add(accountFrom, -1*amount)
Add(accountTo, 1*amount)
return OK
}
假設Add操作的平均響應時延是50ms,那麼我們的Transfer操作平均實驗大約是100ms。在實際運行過程中,每處理一次Transfer操作,需要耗時100ms,並且在100ms內需要獨占一個線程,即每個線程每秒最多可以處理10次Transfer操作。
假設我們在一個伺服器上同時打開的線程數量上限是10000,那麼這個伺服器每秒可以處理的請求上限是:10000*10 = 100000次。
當客戶請求數量超過上限時,請求就需要排隊,那麼Transfer操作的響應時延變成:排隊等待時延+處理時延(100ms),即在大量請求的場景中,我們的服務響應時延增加了。
但是,在這種情況下,伺服器的資源並沒有被消耗很多,例如CPU、記憶體、網卡等資源都很空閑,我們的100000個線程大部分時間在等待Add操作返回結果。
上面就是同步設計方式,在這種情況下,整個伺服器的所有線程大部分時間都沒有在工作,而是在等待。
非同步設計流程
對於同樣的業務場景,我們來看一下非同步方式下的偽代碼:
TransferAsync(accountFrom, accountTo, amount, OnComplete){
AddAsync(accountFrom, -1*amount, OnDebit(accountTo, 1*amount, OnAllDone(OnComplete)))
}
OnDebit(amountTo, amount, OnAllDone(OnComplete)){
AddAsync(accountTo, amount, OnAllDone(OnComplete))
}
OnAllDone(OnComplete){
OnComplete()
}
這裡TransferAsync和之前的Transfer相比,增加了一個參數,這個參數是一個回調方法OnComplete()。
上面方法的語義:請幫我執行轉賬操作,當轉賬完成後,請調用OnComplete()方法,調用TransferAsync的線程不必等待轉賬完成就可以立即返回,當轉賬結束後,OnComplete()方法會被調用來執行後續的工作。
上面代碼中,我們定義了2個回調方法:
- OnDebit():扣減賬戶accountFrom完成後調用的回調方法
- OnAllDone():轉入賬戶accountTop完成後調用的回調方法
整個非同步操作的語義如下:
- 非同步從accountFrom的賬戶中減去相應的錢數,然後調用OnDebit方法
- 在OnDebit方法中,非同步把減去的錢數加到accountTop賬戶中,然後執行OnAllDone方法
- 在OnAllDone方法中,調用OnComplete方法
採用非同步方式後,整個流程的時序和同步實現是完全一樣的,區別在於線程模型由同步順序調用改為非同步調用和回調的機制。
由於流程的時序和同步方式相同,在低請求量的場景下,平均響應時延還是100ms,在超高請求量的場景下,非同步機制不需要線程等待執行結果,只需要個位數的線程,即可實現同步場景大量線程一樣的吞吐量。
在非同步模式下,由於沒有了線程數量上的限制,總體吞吐量上限會大大超過同步實現,並且在伺服器CPU、網路帶寬資源達到極限之前,響應時延不會隨著請求數量增加而顯著升高,幾乎可以一直保持100ms左右的響應時延。
非同步框架:CompletableFuture
Java語言中常用的非同步框架包括CompletableFuture和RxJava,我們主要來看CompletableFuture。
它是Java 8中新增的一個強大的非同步編程的類,包括了我們在開發非同步程式過程中需要的大部分功能。
針對上述的轉賬場景,我們來看一下如何使用CompletableFuture實現。
首先定義2個介面:
public interface AccountService {
CompletableFuture<Void> add(int account, int amount);
}
public interface TransferService {
CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount);
}
然後實現轉賬功能:
public class TransferServiceImpl implements TransferService {
@Inject
private AccountService accountService;
@Override
public CompletableFuture<Void> transfer(int fromAccount, int toAccount, int amount) {
return accountService.add(fromAccount, -1 * amount)
.thenCompose(v -> accountService.add(toAccount, amount));
}
}
客戶端調用TransferService時,可以使用同步方式,也可以使用非同步方式:
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("轉賬完成!"));
}
}
非同步設計的思想:當我們要執行一項比較耗時的操作時,不去等待操作結束,而是給這個操作一個命令:“當操作完成後,接下來去執行什麼”。
使用非同步編程帶來的好處是可以減少或者避免線程等待,只用很少的線程就可以達到超高的吞吐能力。
使用非同步編程帶來的問題是複雜度增加,代碼可讀性和可維護性會下降。
作者:李潘 出處:http://wing011203.cnblogs.com/ 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。