天天看點

TeamsApp更新之路 - 大使用者并發的 Teams http 請求優化

我們這篇文章繼續上篇文章的優化,來看看如何優化 #6。

經過上一篇文章的分析和優化,我們目前的參與抽獎的步驟是:

增加目前使用者到抽獎人清單。一次簡單的寫操作,隻增加目前一個使用者

如果第三步報錯,說明目前這個使用者是否已經參與了本次抽獎,結束這個請求

從 SQL DB 讀取抽獎的基本資訊,不包含參與有人。一次簡單的資料庫讀操作

判斷抽獎是否已經結束,如果已經開獎了,那就結束這個請求

根據目前的抽獎資訊,生成 Teams 前端展示的 adaptive card

調用 Teams 的 bot api,更新本次抽獎的 adaptive card

在正常情況下一次抽獎為:一次資料庫寫操作 (#1),一次資料庫讀操作 (#3),一次 http 請求 (#6)。

如果是使用者反複點選參與抽獎按鈕,那就是一次資料庫寫操作 (#1)。

是以,當我們假設的 3000 使用者在 15 秒内參與了同一個抽獎,那每秒要發送 200 個 http 請求給 teams。Teams 實際上對于發給給它的請求有一些調用頻次的限制,詳細資訊可以參考這個官方文檔。

https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/rate-limit

是以針對這個問題,我們改如何改進?

我準備使用的方法是延遲發送,當我們需要發送的時候,我們先等個 1 到 2 秒時間,如果在這個等待時間裡,有其他的對這個 adaptive card 的更新操作,我們就可以合并到一起,這樣的話,針對某一次抽獎,不管在這 1 到 2 秒時間内有多少人參與抽獎,我們最終隻給 Teams 發送一次更新 activity (adaptive card) 請求。

大家可能覺得說起來容易,如何實作這個呢?這是我使用的延遲發送的示意性代碼:

private static Dictionary<string, DelayContext> _delayContexts = new Dictionary<string, DelayContext>();

public static void UpdateActivityWithDelay()
{
    var delayContext = new DelayContext(...);
    var key = $"{channelId}_{mainActivityId}";

    var first = !_delayContexts.ContainsKey(key);
    _delayContexts[key] = delayContext;
    if (first)
    {
        var thread = new Thread(SendUpdatedActivity);
        thread.Start(key);
    }
}

private static void SendUpdatedActivity(object? keyObject)
{
    Thread.Sleep(DelayUpdateActivityInMilliseconds);

    string key = (string)keyObject!;
    DelayContext? delayContext;
    _delayContexts.TryRemove(key, out delayContext);

    botClient.UpdateActivityAsync();
}
           

可以看到當我們需要發送更新 activity (adaptive card) 的時候調用 UpdateActivityWithDelay 請求,它會把發送的上下文内容儲存到一個清單裡 _delayContexts,然後起一個線程,在這個線程裡先等待 sleep 一些時間,然後再從清單裡擷取發送的上下文,然後發送。

上面之是以說是一個示意性代碼,因為上面代碼完全沒有考慮線程安全,多線程競争等問題。,我們來對它一步步優化。

首先我們要先把清單從 Dictionary<string, DelayContext> 改成 ConcurrentDictionary<string, DelayContext>。

接下來我們要增加一個鎖,防止往清單裡讀寫的多線程競争的情況。如下:

private static object _syncObject = new object();

public static void UpdateActivityWithDelay()
{
    var delayContext = new DelayContext(...);
    var key = $"{channelId}_{mainActivityId}";

    lock(_syncObject)
    {
        var first = !_delayContexts.ContainsKey(key);
        _delayContexts[key] = delayContext;
        if (first)
        {
            var thread = new Thread(SendUpdatedActivity);
            thread.Start(key);
        }
    }
}

private static void SendUpdatedActivity(object? keyObject)
{
    Thread.Sleep(DelayUpdateActivityInMilliseconds);

    string key = (string)keyObject!;
    DelayContext? delayContext;
    lock(_syncObject)
    {
        _delayContexts.TryRemove(key, out delayContext);
    }

    if (delayContext != null)
    {
        botClient.UpdateActivityAsync();
    }
}
           

最後我們就可以把所有我們需要儲存的上下文内容插入到 DelayContext 裡。

全部代碼如下:

private static int DelayUpdateActivityInMilliseconds = 1500;
private static object _syncObject = new object();
private static ConcurrentDictionary<string, DelayContext> _delayContexts = new ConcurrentDictionary<string, DelayContext>();

public static void UpdateActivityWithDelay(IBotClientFactory botClientFactory, IActivityBuilder activityBuilder, string serviceUrl, string channelId, string mainActivityId, Competition competition)
{
    var delayContext = new DelayContext(botClientFactory, activityBuilder, serviceUrl, channelId, mainActivityId, competition);
    var key = $"{channelId}_{mainActivityId}";

    lock(_syncObject)
    {
        var first = !_delayContexts.ContainsKey(key);
        _delayContexts[key] = delayContext;
        if (first)
        {
            var thread = new Thread(SendUpdatedActivity);
            thread.Start(key);
        }
    }
}

private static void SendUpdatedActivity(object? keyObject)
{
    Thread.Sleep(DelayUpdateActivityInMilliseconds);

    string key = (string)keyObject!;
    DelayContext? delayContext;
    lock(_syncObject)
    {
        _delayContexts.TryRemove(key, out delayContext);
    }

    if (delayContext != null)
    {
        using var botClient = delayContext.BotClientFactory.CreateBotClient(delayContext.ServiceUrl);
        var updatedActivity = delayContext.ActivityBuilder.CreateMainActivity(delayContext.Competition);
        botClient.UpdateActivityAsync(delayContext.ChannelId, delayContext.MainActivityId, updatedActivity).GetAwaiter().GetResult();
    }
}
           

我們再來看我們假設的場景:3000使用者在15秒内參與同一個抽獎,經過上面的優化,一共隻需要在15秒内發送 10 個請求給 Teams 就可以了。

總結一下,經過上一篇文章和這篇文章的優化,參與抽獎的流程變成了:

增加目前使用者到抽獎人清單。一次簡單的寫操作,隻增加目前一個使用者

如果第三步報錯,說明目前這個使用者是否已經參與了本次抽獎,結束這個請求

從 SQL DB 讀取抽獎的基本資訊,不包含參與有人。一次簡單的資料庫讀操作

判斷抽獎是否已經結束,如果已經開獎了,那就結束這個請求

延遲 1.5 秒後,根據目前的抽獎資訊,生成 Teams 前端展示的 adaptive card,然後發送 Teams 的 bot api,更新本次抽獎的 acitivity

繼續閱讀