ASP.NET中按Header值注入SemaphoreSlim实现客户端并发限制的正确方式
按请求Header的ClientId限制单客户端并发调用方案
问题分析
你当前的实现存在核心问题:将SemaphoreSlim注册为Scoped服务后,ASP.NET Core的DI容器会在请求结束时自动调用其Dispose方法,但该实例仍存放在全局的ConcurrentDictionary中。后续同一clientId的请求会获取到已被释放的SemaphoreSlim实例,调用WaitAsync或Release时就会抛出异常。
正确实现方案
1. 实现客户端信号量管理器
创建一个Singleton的管理器类,手动管理所有客户端的SemaphoreSlim实例,同时添加定时清理逻辑避免内存泄漏:
public class ClientSemaphoreManager { // 存储每个clientId对应的信号量 private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new(); // 记录每个信号量的最后访问时间 private readonly ConcurrentDictionary<string, DateTime> _lastAccessTimes = new(); // 定时清理定时器 private readonly Timer _cleanupTimer; public ClientSemaphoreManager() { // 每1小时执行一次清理,可根据业务调整 _cleanupTimer = new Timer(CleanupUnusedSemaphores, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); } /// <summary> /// 根据clientId获取对应的信号量 /// </summary> public SemaphoreSlim GetSemaphore(string clientId) { var semaphore = _semaphores.GetOrAdd(clientId, _ => new SemaphoreSlim(1, 1)); // 更新最后访问时间 _lastAccessTimes.AddOrUpdate(clientId, DateTime.UtcNow, (_, _) => DateTime.UtcNow); return semaphore; } /// <summary> /// 清理长时间未使用的信号量 /// </summary> private void CleanupUnusedSemaphores(object? state) { // 清理2小时内未被访问的信号量,可根据业务调整 var cutoffTime = DateTime.UtcNow.Subtract(TimeSpan.FromHours(2)); foreach (var entry in _lastAccessTimes.Where(e => e.Value < cutoffTime).ToList()) { if (_semaphores.TryRemove(entry.Key, out var semaphore)) { semaphore.Dispose(); _lastAccessTimes.TryRemove(entry.Key, out _); } } } }
2. 注册服务到DI容器
在Program.cs中注册管理器为Singleton,并确保IHttpContextAccessor已注册:
// 注册IHttpContextAccessor以获取请求Header services.AddHttpContextAccessor(); // 注册信号量管理器为Singleton services.AddSingleton<ClientSemaphoreManager>();
3. 在业务服务中使用
修改业务服务,注入管理器和IHttpContextAccessor,而非直接注入SemaphoreSlim:
private readonly ClientSemaphoreManager _semaphoreManager; private readonly IHttpContextAccessor _httpContextAccessor; public YourBusinessService(ClientSemaphoreManager semaphoreManager, IHttpContextAccessor httpContextAccessor) { _semaphoreManager = semaphoreManager; _httpContextAccessor = httpContextAccessor; } public async Task SomeMethodAsync() { // 获取请求Header中的clientId,无值则用"0"兜底 var clientId = _httpContextAccessor.HttpContext?.Request.Headers["clientId"].ToString() ?? "0"; var semaphore = _semaphoreManager.GetSemaphore(clientId); await semaphore.WaitAsync(); try { // 执行你的异步业务逻辑 await Task.Delay(1000); } finally { semaphore.Release(); } }
关键说明
- 避免DI自动释放:
SemaphoreSlim不再由DI容器管理生命周期,而是由管理器手动创建和释放,彻底解决已释放实例被重复使用的问题。 - 内存泄漏防护:通过定时清理长时间未使用的信号量,避免大量闲置的clientId导致内存占用过高。
- 并发安全:使用
ConcurrentDictionary保证多线程下的信号量获取和更新操作安全。
内容的提问来源于stack exchange,提问作者Josue David Lima Garcia
相关产品推荐
相关产品推荐

