我們這篇文章繼續上篇文章的優化,來看看如何優化 #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