[效能調教] 使用 Async / Await 非同步機制加快 Web API 回應時間

在 Web API 執行過程中可能需要一次對資料庫提出數個操作需求,當這些查詢彼此沒有順序相依性時,就可以善用非同步的機制讓所有查詢盡可能一次送出,而最終只需花費單一最長查詢所需要的時間即可獲得所有查詢的結果,對於效能的提升相當有幫助。

前言


在效能調教的過程中,絕大部分除了探討 DB 操作的時間消耗外,我們還可以利用非同步的概念節省一些不必要的時間耗費;非同步的概念如下圖所示,假設我們的目標就是要執行 DoSP1 及 DoSP2 兩隻預存程式,當在同步的情況下,在執行 DoSP1 後,只能等待資料處理完畢回傳時,才能接續執行 DoSP2 並等待其完成作業,假設各別花費 1 秒的時間,我們就必須花費 2 秒完成這整個交易行為;但是在非同步的情況下,在執行 DoSP1Async 後不必等待其處理完畢,直接就可以再執行 DoSP2Async 作業,最終由於處理時間的重疊,我們可以僅花費 1.1 秒就完成相同交易行為。由此例子來看,若當整個交易行為內的這些 DB 操作沒有明確的順序相依性,可以善用非同步機制大幅度加快回應時間。以下將就此例進行實作驗證。

 

事前準備


假設目前存在兩隻 Stored Procedure 分別為 usp_async_test_1 及 usp_async_test_2,為模擬處理時間則都在 SP 中等待 1 秒鐘,代碼如下。

-- SP1
CREATE PROCEDURE [dbo].[usp_async_test_1]
AS BEGIN
	SET NOCOUNT ON;
	WAITFOR DELAY '00:00:01'
	SELECT 1
END

-- SP2
CREATE PROCEDURE [dbo].[usp_async_test_2]
AS BEGIN
	SET NOCOUNT ON;
	WAITFOR DELAY '00:00:01'
	SELECT 2
END

建立同步及非同步呼叫 Stored Procedure 的方法如下。

public IDbConnection CreateConnection()
{
    return new SqlConnection(str);
}

private int DoSP1(IDbConnection cn)
{
    var result = cn.Query<int>("usp_async_test_1", commandType: CommandType.StoredProcedure);
    return result.FirstOrDefault();
}

private int DoSP2(IDbConnection cn)
{
    var result = cn.Query<int>("usp_async_test_2", commandType: CommandType.StoredProcedure);
    return result.FirstOrDefault();
}

private async Task<int> DoSP1Async(IDbConnection cn)
{
    var result = await cn.QueryAsync<int>("usp_async_test_1", commandType: CommandType.StoredProcedure);
    return result.FirstOrDefault();
}

private async Task<int> DoSP2Async(IDbConnection cn)
{
    var result = await cn.QueryAsync<int>("usp_async_test_2", commandType: CommandType.StoredProcedure);
    return result.FirstOrDefault();
}

 

同步作業


使用同步方式執行 SP 的 Web API 代碼如下

public IHttpActionResult Get()
{
    var sp1Result = DoSP1(CreateConnection());
    var sp2Result = DoSP2(CreateConnection());

    return Ok();
}

透過 JMeter 計算執行時間如下,結果如預期花費 2011 ms,也就是 DoSP1 花了 1 秒再加上 DoSP2 花了 1 秒的疊加結果。

 

非同步作業


使用非同步方式執行 SP 的 Web API 代碼如下

public async Task<IHttpActionResult> Get()
{
    // 假設 sp1 與 sp2 沒有順序相依性
  

    // 非同步執行呼叫 SP1
    var sp1Task = DoSP1Async(CreateConnection());

    // 非同步執行呼叫 SP2
    var sp2Task = DoSP2Async(CreateConnection());


    // ....


    // 在此等待 sp1 結果回傳 (約花費 1 秒等待時間)
    var sp1Result = await sp1Task;

    // 在此等待 sp2 結果回傳 (在等待 sp1 處理的時間,sp2 也已經完成了,無產生等待時間)
    var sp2Result = await sp2Task;


    return Ok();
}

透過 JMeter 計算執行時間如下,結果如預期僅花費 1012 ms,也就是在等待 DoSP1 的同時間, DoSP2 也正在執行中,而此情境兩者花費的時間相同,因此當 DoSP1 處理完畢時, DoSP2 也完成作業了。

 

偽非同步作業


有時候會常看到以下這種代碼,在呼叫非同步作業後立即等待取回資料;若是兩者有絕對順序相依(ex. 須將 DoSP1 處理完後的值帶入 DoSP2 執行),那當然無話可說,但如果只因不熟悉運作機制,而寫出以下使用情境,那我們來看看有什麼結果。

public async Task<IHttpActionResult> Get()
{

    // 非同步執行呼叫 SP1
    var sp1Task = DoSP1Async(CreateConnection());

    // 在此等待 sp1 結果回傳 (約花費 1 秒等待時間)
    var sp1Result = await sp1Task;



    // 非同步執行呼叫 SP2
    var sp2Task = DoSP2Async(CreateConnection());

    // 在此等待 sp2 結果回傳 (約花費 1 秒等待時間)
    var sp2Result = await sp2Task;


    return Ok();
}

透過 JMeter 計算執行時間如下,結果如同步結果也花費 2011 ms,其實這就是同步作業。

 

結論


以最後一個測試為例,並非所有方法都套上非同步就可以達到效能提升的好處,而是需要了解情境是否合宜,並且在情境允許下,對程式做出最妥善的規劃;例如沒有順序相依性的 DB 查詢都可以在程式開頭以非同步的方式全部執行,後續才去處理一些邏輯或需依序執行的 DB 操作,如此才可以發揮非同步執行的最大效益。

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !