在高併發的 Web API 環境中,瞬間湧入的請求就像演唱會開賣時的粉絲潮水,沒有控管就會把系統直接「擠爆」。
這篇文章要分享一個我在 ASP.NET Core 9 專案中實作的 限流 + 排隊機制,它不只保護後端服務,還能讓用戶感覺到「公平」。

開發環境
- Windows 11 + WSL2 Ubuntu
- ASP.NET Core 9
- Rider 2025.2
系統架構設計
核心業務流程
graph TD
A[使用者請求 POST /api/commands] --> B[前置條件檢查]
B --> C{檢查佇列是否已滿<br/>_commandQueue.IsQueueFull}
C -->|佇列已滿| D[加入佇列並回傳 429<br/>Reason: QueueFull]
C -->|佇列未滿| E{檢查限流<br/>!_rateLimiter.IsAllowed}
E -->|限流失敗| F[加入佇列並回傳 429<br/>Reason: Rate Limit Exceeded]
E -->|限流通過| G[記錄請求<br/>_rateLimiter.RecordRequest]
G --> H[直接執行業務邏輯]
H --> I[回傳 200 OK]
D --> J[客戶端根據 RetryAfterSeconds 等待重試]
F --> J
J --> A
狀態機
stateDiagram-v2
[*] --> Queued : POST /api/commands<br/>(超過限流)
Queued --> Ready : 背景服務<br/>許可管理
Queued --> Failed : 系統錯誤<br/>或取消
Ready --> Processing : 調用 /wait 端點<br/>開始執行
Ready --> Failed : 執行失敗<br/>或超時
Processing --> Finished : 執行完成<br/>移出佇列
Processing --> Failed : 執行過程<br/>發生錯誤
Failed --> [*] : 清理失敗記錄
Finished --> [*] : 記錄到<br/>cleanup-summary

關鍵技術實作
限流演算
這是整個系統的核心保護機制,使用 ConcurrentQueue<DateTime> 實作 SlidingWindowRateLimiter:
public class SlidingWindowRateLimiter : IRateLimiter
{
private readonly ConcurrentQueue<DateTime> _requestTimestamps = new();
private readonly int _maxRequests;
private readonly TimeSpan _timeWindow;
private readonly object _lock = new();
public bool IsAllowed()
{
lock (_lock)
{
CleanupOldRequests(); // 清理過期的時間戳
return _requestTimestamps.Count < _maxRequests;
}
}
public void RecordRequest()
{
lock (_lock)
{
_requestTimestamps.Enqueue(DateTime.UtcNow);
}
}
private void CleanupOldRequests()
{
var cutoffTime = DateTime.UtcNow - _timeWindow;
while (_requestTimestamps.TryPeek(out var timestamp) && timestamp < cutoffTime)
{
_requestTimestamps.TryDequeue(out _);
}
}
}
Channel 佇列管理
使用 .NET 的 Channel<T> 實作生產者-消費者佇列
public class ChannelCommandQueueProvider : ICommandQueueProvider
{
private readonly Channel<QueuedContext> _channel;
private readonly ConcurrentDictionary<string, QueuedContext> _pendingRequests = new();
private readonly int _maxCapacity;
public ChannelCommandQueueProvider(int capacity = 100)
{
_maxCapacity = capacity;
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = false,
SingleWriter = false
};
_channel = Channel.CreateBounded<QueuedContext>(options);
}
public async Task<string> EnqueueCommandAsync(object requestData, CancellationToken cancel = default)
{
var queuedRequest = new QueuedContext
{
RequestData = requestData
};
_pendingRequests[queuedRequest.Id] = queuedRequest;
await _writer.WriteAsync(queuedRequest, cancel);
return queuedRequest.Id;
}
}
把列隊內的狀態壓成 Ready,不執行業務邏輯
public class ReadyCommandQueueService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var queuedRequest = await _commandQueue.GetNextQueuedRequestAsync(stoppingToken);
if (queuedRequest == null)
{
await Task.Delay(_emptyQueueDelay, stoppingToken);
continue;
}
// 等待直到可以許可請求(遵守限流規則)
while (!_rateLimiter.IsAllowed())
{
var retryAfter = _rateLimiter.GetRetryAfter();
await Task.Delay(retryAfter.Add(TimeSpan.FromMilliseconds(100)), stoppingToken);
}
// 只記錄許可並更新狀態為 Ready,不執行業務邏輯
_rateLimiter.RecordRequest();
await _commandQueue.MarkRequestAsReadyAsync(queuedRequest.Id, stoppingToken);
}
}
}
定期清理超過指定時間且未被取得結果的過期請求
為了避免卡了太多未處理的請求,這個背景服務專門用來清理門戶
public class ExpiredRequestCleanupService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("ExpiredRequestCleanupService started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(_cleanupInterval, stoppingToken);
var cleanedCount = _queueProvider.CleanupExpiredRequests(_maxRequestAge);
if (cleanedCount > 0)
{
_logger.LogInformation(
"Cleaned up {CleanedCount} expired requests older than {MaxRequestAge}",
cleanedCount,
_maxRequestAge);
}
else
{
_logger.LogDebug("No expired requests found during cleanup cycle");
}
}
}
}
}
API 端點設計
主要端點
POST /api/commands
[HttpPost("commands")]
public async Task<IActionResult> CreateCommandAsync([FromBody] CreateCommandRequest request)
{
// 前置條件檢查
var isQueueFull = _commandQueue.IsQueueFull();
if (isQueueFull)
{
var requestId = await _commandQueue.EnqueueCommandAsync(request.Data, cancel);
return StatusCode(429, new
{
Message = "Too many requests. Queue is full, please retry after the specified time.",
RequestId = requestId,
Reason = "QueueFull"
});
}
var isRateLimitExceeded = !_rateLimiter.IsAllowed();
if (isRateLimitExceeded)
{
var requestId = await _commandQueue.EnqueueCommandAsync(request.Data, cancel);
return StatusCode(429, new
{
RequestId = requestId,
Reason = "Rate Limit Exceeded"
});
}
// 通過所有檢查,直接執行
_rateLimiter.RecordRequest();
var response = await ExecuteBusinessLogicAsync(request, cancel);
return Ok(response);
}
GET /api/commands/{requestId}/wait
[HttpGet("commands/{requestId}/wait")]
public async Task<IActionResult> WaitForCommandAsync(string requestId)
{
var queuedRequest = await _commandQueue.ExecuteReadyRequestAsync(requestId, cancel);
if (queuedRequest?.Status == QueuedCommandStatus.Processing)
{
var response = await ExecuteBusinessLogicAsync(queuedRequest, cancel);
await _commandQueue.FinishAndRemoveRequestAsync(queuedRequest, response, cancel);
return Ok(response);
}
return StatusCode(429, new { Message = "Request is not ready for execution" });
}
系統配置
在 Program.cs 中的關鍵配置:
// 限流器設定(10秒內最多2個請求)
builder.Services.AddSingleton<IRateLimiter>(provider =>
new SlidingWindowRateLimiter(maxRequests: 2, timeWindow: TimeSpan.FromSeconds(10)));
// 佇列提供者(容量100)
builder.Services.AddSingleton<ICommandQueueProvider>(provider =>
new ChannelCommandQueueProvider(capacity: 100));
// 每 5 秒掃描一次,超過一分鐘未處理的,則拋棄
builder.Services.AddHostedService<ExpiredRequestCleanupService>(provider =>
new ExpiredRequestCleanupService(
provider.GetRequiredService<ICommandQueueProvider>(),
provider.GetRequiredService<ILogger<ExpiredRequestCleanupService>>(),
TimeSpan.FromMinutes(1),
TimeSpan.FromSeconds(5)));
實際測試驗證
正常流量
# 第1個請求:直接執行
curl -X POST https://localhost:7131/api/commands -d '{"data":"user1_request"}'
# 回應:200 OK
# 第2個請求:直接執行
curl -X POST https://localhost:7131/api/commands -d '{"data":"user2_request"}'
# 回應:200 OK
觸發限流
# 第3個請求:觸發限流,進入佇列
curl -X POST https://localhost:7131/api/commands -d '{"data":"user3_request"}'
# 回應:429 Too Many Requests
# {
# "RequestId": "abc123",
# "Reason": "Rate Limit Exceeded",
# "RetryAfterSeconds": 8
# }
# 等待背景服務處理...
sleep 10
# 主動執行
curl https://localhost:7131/api/commands/abc123/wait
# 回應:200 OK(業務邏輯執行結果)
搶票系統應用場景
這個系統非常適合搶票類應用:
演唱會售票流程
- 開搶瞬間:大量使用者同時請求
- 系統保護:前N個直接處理,其餘進入佇列
- 排隊等待:使用者收到排隊位置和預估等待時間
- 主動搶票:輪到使用者時,主動執行搶票動作
- 完成記錄:所有操作都有完整的歷史記錄
關鍵優勢
- 系統穩定:限流保護避免服務崩潰
- 用戶體驗:透明的排隊狀態和主動控制
- 公平機制:先到先服務的排隊邏輯
- 擴展性:分離架構易於水平擴展
總結
這個範例展示了如何使用 ASP.NET Core 構建一個既能保護系統又能提供良好用戶體驗的解決方案。透過限流 + Channel 佇列管理和背景許可服務的結合,完成了現流 + 排隊的功能
在實際專案中,你可以根據業務需求調整限流參數、佇列容量和處理邏輯,這個範例只是提供一個思路,千萬不要照抄。
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/Rate%20Limit/Lab.QueueApi
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET