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

如何锁定同IP的外部API调用,避免多线程重复消耗配额?

解决方案:利用并发字典实现同一IP的请求合并

要解决同一IP多线程重复调用外部API的问题,核心思路是让同一个IP的所有并发请求共享同一个API调用任务,只有第一个发起请求的线程去调用外部API,其他线程等待该任务完成后直接复用结果。具体实现可以借助ConcurrentDictionary来缓存正在执行的API请求任务,以下是修改后的完整代码和关键说明:

修改后的服务代码

// Registered as Singleton in ASP.NET Core DI
public class IPInfoService(
    IServiceProvider serviceProvider,
    IConfiguration configuration,
    IHttpClientFactory httpClientFactory)
{
    private readonly string _apiKey = configuration["MyApiKey"] ?? "";
    private readonly HttpClient _httpClient = httpClientFactory.CreateClient();
    // 缓存正在进行的API请求任务,键为IP地址
    private readonly ConcurrentDictionary<string, Task<IPInfoDTO>> _pendingApiRequests = new();

    public async Task<IPInfoDTO> GetIPInfoAsync(string ipString)
    {
        // 第一步:检查本地数据库是否有有效记录
        using var scope = serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<IpInfoDbContext>();

        var dbRow = await dbContext.IpInfos
            .FirstOrDefaultAsync(x => x.IpAddress == ipString && x.ExpiresAt > DateTime.UtcNow);
        if (dbRow != null)
        {
            return MapToDTO(dbRow);
        }

        // 第二步:尝试获取或创建当前IP的API请求任务
        var apiTask = _pendingApiRequests.GetOrAdd(ipString, async (ip) =>
        {
            try
            {
                // 双重检查数据库:防止在任务创建期间已有其他线程完成API调用并写入数据
                using var taskScope = serviceProvider.CreateScope();
                var taskDbContext = taskScope.ServiceProvider.GetRequiredService<IpInfoDbContext>();
                
                var existingRow = await taskDbContext.IpInfos
                    .FirstOrDefaultAsync(x => x.IpAddress == ip && x.ExpiresAt > DateTime.UtcNow);
                if (existingRow != null)
                {
                    return MapToDTO(existingRow);
                }

                // 调用外部API
                var response = await _httpClient.GetAsync($"https://your-external-api.com/ip/{ip}?key={_apiKey}");
                response.EnsureSuccessStatusCode();
                var ipInfo = await response.Content.ReadFromJsonAsync<IPInfoDTO>() 
                    ?? throw new InvalidOperationException("API返回空数据");

                // 将结果写入数据库
                taskDbContext.IpInfos.Add(new IpInfoEntity
                {
                    IpAddress = ip,
                    // 映射DTO字段到实体
                    Country = ipInfo.Country,
                    City = ipInfo.City,
                    ExpiresAt = DateTime.UtcNow.AddMonths(1)
                });
                await taskDbContext.SaveChangesAsync();

                return ipInfo;
            }
            finally
            {
                // 任务完成(成功/失败)后移除缓存,避免内存泄漏
                _pendingApiRequests.TryRemove(ip, out _);
            }
        });

        // 等待API任务完成,返回结果
        return await apiTask;
    }

    // 辅助方法:将数据库实体映射为DTO
    private IPInfoDTO MapToDTO(IpInfoEntity entity)
    {
        return new IPInfoDTO
        {
            IpAddress = entity.IpAddress,
            Country = entity.Country,
            City = entity.City
            // 补充其他字段
        };
    }
}

关键实现说明

  • ConcurrentDictionary原子操作:GetOrAdd是线程安全的原子方法,确保同一个IP只会创建一个API请求任务,后续线程直接复用已有的任务,完全避免重复调用外部API。
  • 双重数据库检查:在任务内部再次查询数据库,防止在第一个线程创建任务的短暂窗口中,其他线程已经完成了API调用并写入数据,进一步避免不必要的API请求。
  • 独立Scope管理DbContext:单例服务中不能共享DbContext,因此在异步任务内部单独创建Scope获取DbContext,保证DbContext的线程安全性。
  • 任务清理:通过finally块在任务完成后移除缓存的任务,避免字典内存泄漏,同时让后续失败的IP请求可以重新发起API调用。
  • HttpClient优化:改用IHttpClientFactory创建HttpClient,替代直接new的方式,避免长期运行导致的Socket资源耗尽问题。

额外注意事项

  • 可以根据需求为API调用添加重试逻辑(比如用Polly库),应对外部API的临时故障。
  • 如果API调用失败,所有等待该任务的线程都会收到相同的异常,你可以在catch块中处理异常,返回默认值或自定义错误信息。

内容的提问来源于stack exchange,提问作者Parsa99

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 09:23:13