天天看點

消息隊列面試解析系列(六)- 異步程式設計妙用(上)0 異步的優勢1 案例引入2 同步的性能瓶頸異步實作方案

0 異步的優勢

太多的線程會造成頻繁的cpu上下文切換,你可以想象一下,假設你的小公司隻有8台電腦,你雇8個程式員一直不停的工作顯然是效率最高的。考慮到程式員要休息不可能連軸轉,雇傭24個人,每天三班倒,效率也還行。

但是,你要雇傭10000個人,他們還是隻能用這8台電腦,大部分時間不都浪費在換人、交接工作上啦。

異步程式設計是通過分工的方式,是為了減少了cpu因線程等待的可能,讓CPU一直處于工作狀态。換句話說,如果我們能想辦法減少CPU空閑時間,我們的計算機就可以支援更多的線程。

其實線程是一個抽象概念,我們從實體層面了解,就是機關時間内把每毫核配置設定處理不同的任務,進而提高機關時間内CPU的使用率。

線程就是為了能自動配置設定CPU時間片而生的。

異步實作裡面還是要用線程池限制一下線程數吧,否則沒有達到減少線程的效果。

異步模式設計的程式可以顯著減少線程等待,進而在高吞吐量的場景中,極大提升系統的整體性能,顯著降低延遲時間。

是以,像MQ這種需要超高吞吐量和超低延遲時間的中間件系統,在其核心流程中,會大量采用異步設計。

1 案例引入

一個轉賬的微服務Transfer( accountFrom, accountTo, amount),該服務有三參數

  • 轉出賬戶
  • 轉入賬戶
  • 轉賬金額

要從賬戶A中轉賬100到賬戶B:

  1. 先從A的賬戶中減去100元
  2. 再給B的賬戶加上100元,轉賬完成。

對應時序圖:

消息隊列面試解析系列(六)- 異步程式設計妙用(上)0 異步的優勢1 案例引入2 同步的性能瓶頸異步實作方案

調用另外一個微服務Add(account, amount),給賬戶account增加金額amount,當amount為負值時,就是扣減相應金額。

為簡化,省略了錯誤處理和事務相關代碼

2 同步的性能瓶頸

同步實作,對應的僞代碼:

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()方法來執行轉賬後續工作。

異步實作相對同步稍複雜。先定義倆回調方法:

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

異步實作語義相當于:

  1. 異步從accountFrom的賬戶中減去相應錢數,然後調用OnDebit
  2. 在OnDebit方法中,異步把減去的錢數加到accountTo的賬戶中,然後執行OnAllDone
  3. 在OnAllDone中調用OnComplete

時序圖

消息隊列面試解析系列(六)- 異步程式設計妙用(上)0 異步的優勢1 案例引入2 同步的性能瓶頸異步實作方案

異步後,整個流程時序和同步完全一樣,隻是線程模型由同步調用改為異步和回調。