You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 00:23:12