怎麼利用異步設計提升系統性能?

異步是一種程序設計的思想,使用異步模式設計的程序可以顯著減少線程等待,從而在高吞吐量的場景中,極大提升系統的整體性能,降低請求時延。

同步設計流程

我們假設要做一個轉賬的業務,即從賬戶A中轉賬100元到賬戶B中,它包含2步:

  1. 從A的賬戶中減少100元
  2. 給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個回調方法:

  1. OnDebit():扣減賬戶accountFrom完成後調用的回調方法
  2. OnAllDone():轉入賬戶accountTop完成後調用的回調方法

整個異步操作的語義如下:

  1. 異步從accountFrom的賬戶中減去相應的錢數,然後調用OnDebit方法
  2. 在OnDebit方法中,異步把減去的錢數加到accountTop賬戶中,然後執行OnAllDone方法
  3. 在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("轉賬完成!"));
    }
}

異步設計的思想:當我們要執行一項比較耗時的操作時,不去等待操作結束,而是給這個操作一個命令:“當操作完成後,接下來去執行什麼”。

使用異步編程帶來的好處是可以減少或者避免線程等待,只用很少的線程就可以達到超高的吞吐能力。

使用異步編程帶來的問題是複雜度增加,代碼可讀性和可維護性會下降。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章