使用Service Bus Topic触发器的Azure Function注册JanrainApiClient单例时遭遇重复键异常求助
问题分析
你的异常根源在于单例的JanrainApiClient持有了一个全局共享的HttpClient实例。HttpClient的BaseAddress、DefaultRequestHeaders这类配置属性并不是线程安全的,当Service Bus Topic触发多个并发的Function执行时,多个请求会同时修改这个共享HttpClient的属性:
- 并发修改Header会导致内部字典的键冲突,抛出
An item with the same key has already been added错误 - 当
HttpClient已经发起请求后再修改配置,会触发This instance has already started one or more requests异常
解决方案
这里提供两种可行的解决方式,推荐第二种更优雅的实现:
方案1:每次请求创建独立的HttpClient实例
修改JanrainApiClient,不在构造函数中保存HttpClient,而是每次调用API时从IHttpClientFactory创建新实例,确保每个请求的配置都是独立的:
public class JanrainApiClient : IJanrainApiClient { private readonly ILogger<JanrainApiClient> logger; private readonly IHttpClientFactory httpClientFactory; private readonly JanrainConfigOption janrainConfigOption; public JanrainApiClient(ILogger<JanrainApiClient> logger, IOptions<JanrainConfigOption> janrainConfigOption, IHttpClientFactory httpClientFactory) { this.logger = logger; this.janrainConfigOption = janrainConfigOption.Value; this.httpClientFactory = httpClientFactory; // 仅保存工厂实例 } public async Task<JanrainResultDto> FindAsync(string userUUID) { try { logger.LogInformation("{class} - {method} - Start", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync)); JanrainResultDto resultDto = new JanrainResultDto(); var request = GetJanrainApiRequest(userUUID); var serviceUrl = GetServiceUrl(request); // 每次请求创建新的HttpClient,确保配置独立 using var httpClient = httpClientFactory.CreateClient(); httpClient.BaseAddress = new Uri(serviceUrl); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(janrainConfigOption.Scheme, janrainConfigOption.ApiAuth); var response = await httpClient.GetAsync(serviceUrl); var jsonAsString = await response.Content.ReadAsStringAsync(); JanrainApiResponse entityFindResponse = jsonAsString.AsPoco<JanrainApiResponse>(); if (entityFindResponse != null && entityFindResponse.Results != null) { resultDto = entityFindResponse.Results.FirstOrDefault(); } else { logger.LogError($"Error while requesting Janrain info for UserUUId:{userUUID}. Error:{entityFindResponse?.Error}, ErrorMessage:{entityFindResponse?.ErrorDescription}"); } logger.LogInformation("{class} - {method} - End", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync)); return resultDto; } catch (Exception ex) { logger.LogError($"Exception while calling Janrain entity Find API for UserUUId :{userUUID}, Error message:{ex.Message}, StackTrace:{ex.StackTrace}"); throw; } } // 其他方法保持不变 }
方案2:使用命名HttpClient(推荐)
通过IHttpClientFactory配置命名的HttpClient,预先设置好所有固定配置,避免每次请求重复修改属性,同时利用工厂管理HttpClient的生命周期,避免Socket资源耗尽:
第一步:修改Startup.cs配置命名HttpClient
public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { builder.Services.AddLogging(); // 配置专用于Janrain的命名HttpClient builder.Services.AddHttpClient("JanrainClient", (serviceProvider, client) => { var config = serviceProvider.GetRequiredService<IOptions<JanrainConfigOption>>().Value; // 如果Janrain的API基础地址固定,可以直接设置BaseAddress // client.BaseAddress = new Uri(config.EntityFindApiUrl); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(config.Scheme, config.ApiAuth); }); builder.Services.AddSingleton<IJanrainApiClient, JanrainApiClient>(); } }
第二步:修改JanrainApiClient注入命名HttpClient
public class JanrainApiClient : IJanrainApiClient { private readonly ILogger<JanrainApiClient> logger; private readonly HttpClient httpClient; private readonly JanrainConfigOption janrainConfigOption; // 注入预先配置好的命名HttpClient public JanrainApiClient(ILogger<JanrainApiClient> logger, IOptions<JanrainConfigOption> janrainConfigOption, IHttpClientFactory httpClientFactory) { this.logger = logger; this.janrainConfigOption = janrainConfigOption.Value; this.httpClient = httpClientFactory.CreateClient("JanrainClient"); } public async Task<JanrainResultDto> FindAsync(string userUUID) { try { logger.LogInformation("{class} - {method} - Start", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync)); JanrainResultDto resultDto = new JanrainResultDto(); var request = GetJanrainApiRequest(userUUID); var serviceUrl = GetServiceUrl(request); // 直接使用预配置的HttpClient,无需再修改任何属性 var response = await httpClient.GetAsync(serviceUrl); var jsonAsString = await response.Content.ReadAsStringAsync(); JanrainApiResponse entityFindResponse = jsonAsString.AsPoco<JanrainApiResponse>(); if (entityFindResponse != null && entityFindResponse.Results != null) { resultDto = entityFindResponse.Results.FirstOrDefault(); } else { logger.LogError($"Error while requesting Janrain info for UserUUId:{userUUID}. Error:{entityFindResponse?.Error}, ErrorMessage:{entityFindResponse?.ErrorDescription}"); } logger.LogInformation("{class} - {method} - End", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync)); return resultDto; } catch (Exception ex) { logger.LogError($"Exception while calling Janrain entity Find API for UserUUId :{userUUID}, Error message:{ex.Message}, StackTrace:{ex.StackTrace}"); throw; } } // 其他方法保持不变 }
为什么方案2更好?
IHttpClientFactory会自动管理HttpClient的Handler生命周期,避免传统单例HttpClient导致的Socket资源耗尽问题- 预先配置好的参数无需每次请求重复设置,减少代码冗余
- HttpClient的请求方法(如
GetAsync)是线程安全的,预配置后无需修改属性,完全避免并发冲突
内容的提问来源于stack exchange,提问作者Rakesh Kumar
相关产品推荐
相关产品推荐

