Azure Function:用IMemoryCache存Bearer Token还是Polly处理401错误?
基于Service Bus Topic Trigger的Azure Function 401错误处理方案选型指导
问题背景
我正在开发基于Service Bus Topic Trigger的Azure Function,调用外部API时需要处理401(未授权)错误,包括刷新Bearer Token并重试失败请求。目前考虑两种方案,不确定哪种更合适,希望获得选型指导。
现有实现代码
Polly重试策略类
public class PollyRetryPolicy : IPollyRetryPolicy { private readonly ILogger<PollyRetryPolicy> _logger; private readonly IOptions<RetryPolicyConfigOptions> _options; private readonly Random _jitterer; // 补充缺失的字段定义 public PollyRetryPolicy(ILogger<PollyRetryPolicy> logger, IOptions<RetryPolicyConfigOptions> options) { _logger = logger; _options = options; _jitterer = new Random(); } public IAsyncPolicy GetUnauthorizedRetryPolicy() { var policy = Policy .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized) .WaitAndRetryAsync( _options.Value.MaxRetryAttempts, retryAttempt => TimeSpan.Zero); // 设置延迟为0毫秒,立即重试 return policy; } }
API客户端类(使用IMemoryCache存储Bearer Token)
public class NotificationApiClient : INotificationApiClient { private readonly ILogger<NotificationApiClient> _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly IMemoryCache _memoryCache; private readonly NotificationApiConfigOption _notificationApiConfigOption; public NotificationApiClient(ILogger<NotificationApiClient> logger, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, IOptions<NotificationApiConfigOption> notificationApiConfigOption) { _logger = logger; _httpClientFactory = httpClientFactory; _memoryCache = memoryCache; _notificationApiConfigOption = notificationApiConfigOption.Value; } public async Task<bool> CheckDeliveryAccessAsync(int code) { try { var httpClient = _httpClientFactory.CreateClient(); httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri); // 获取Bearer Token string bearerToken = await GetNotificationAccessTokenAsync(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); var apiResponse = await httpClient.GetAsync($"{_notificationApiConfigOption.CheckDeliveryAccessApiUri}?code={code}"); if (apiResponse.StatusCode == HttpStatusCode.OK) { string resultContent = await apiResponse.Content.ReadAsStringAsync(); var result = resultContent.AsPoco<CheckInvoiceDeliveryAccessResponse>(); return result.Data.IsActive; } else { return false; } } catch (Exception ex) { throw; } } public async Task<string> GetNotificationAccessTokenAsync() { try { if (_memoryCache.TryGetValue("BearerToken", out string cachedBearerToken)) { return cachedBearerToken; } var httpClient = _httpClientFactory.CreateClient(); httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri); var formData = new Dictionary<string, string> { { "clientId", _notificationApiConfigOption.ClientId }, { "clientSecret", _notificationApiConfigOption.ClientSecret } }; var content = new FormUrlEncodedContent(formData); var apiResponse = await httpClient.PostAsync($"/{_notificationApiConfigOption.AccessTokenUri}", content); if (apiResponse.StatusCode == HttpStatusCode.OK) { string resultContent = await apiResponse.Content.ReadAsStringAsync(); var result = resultContent.AsPoco<NotificationAccessTokenResponse>(); _memoryCache.Set("BearerToken", result.AccessToken, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result.ExpiresIn) }); return result.AccessToken; } else { return string.Empty; } } catch (Exception ex) { throw; } } }
方案选型分析
方案1:在Polly重试策略中集成Token刷新逻辑
实现思路:修改Polly的WaitAndRetryAsync逻辑,在每次重试前触发Token刷新(或清除缓存让GetNotificationAccessTokenAsync自动重新获取),再执行请求。
优势:
- 重试策略与Token管理职责分离,Polly专注于容错控制,客户端专注于API交互
- 配置灵活,可通过Polly快速调整重试次数、延迟策略(不建议用0延迟,加小抖动避免并发刷新冲突)
- 复用Polly成熟的容错机制,后续扩展其他错误处理(如5xx)成本低
注意点:
- 当前Polly仅处理
HttpRequestException,但现有客户端遇到401时不会抛出异常(直接返回false),导致策略不生效。需修改客户端,遇到401时主动抛出HttpRequestException,或让Polly直接检测响应状态码。 - 需避免并发刷新Token的竞态问题:多个请求同时触发401时,不要重复调用Token接口,可在
GetNotificationAccessTokenAsync中加锁,或用Polly的Bulkhead限制并发刷新。
方案2:在API客户端内部处理401重试和Token刷新
实现思路:在CheckDeliveryAccessAsync方法中,检测到401响应时,直接刷新Token(清除缓存)并重新执行请求,自行控制重试次数。
优势:
- 逻辑集中在客户端,无需依赖Polly,适合简单场景
- 代码紧凑,无需额外配置策略
注意点:
- 需自行实现重试计数、延迟逻辑,不如Polly成熟可靠
- 多API方法需重复实现相同逻辑,代码冗余
- 同样要处理并发刷新Token的竞态问题
推荐方案
优先选方案1,理由如下:
- Polly作为专业容错库,提供了重试、熔断、隔离等完善机制,后续扩展其他容错需求成本极低
- 职责分离清晰,客户端负责API交互和Token管理,Polly负责重试策略,代码结构更易维护
- 调整重试规则只需修改Polly配置,无需改动业务代码
优化建议
- 修正Polly触发条件:修改客户端,遇到401时抛出异常,确保Polly能捕获:
var apiResponse = await httpClient.GetAsync($"{_notificationApiConfigOption.CheckDeliveryAccessApiUri}?code={code}"); if (apiResponse.StatusCode == HttpStatusCode.Unauthorized) { throw new HttpRequestException("未授权", null, HttpStatusCode.Unauthorized); } else if (apiResponse.StatusCode == HttpStatusCode.OK) { // 现有逻辑 } else { return false; }
- 添加带抖动的重试延迟:替换0延迟,用指数退避+抖动避免并发压力:
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(_jitterer.Next(0, 1000))
- 解决Token刷新竞态:在
GetNotificationAccessTokenAsync中用双重检查锁:
private static readonly object _lockObj = new object(); public async Task<string> GetNotificationAccessTokenAsync() { if (_memoryCache.TryGetValue("BearerToken", out string cachedBearerToken)) { return cachedBearerToken; } lock (_lockObj) { if (_memoryCache.TryGetValue("BearerToken", out cachedBearerToken)) { return cachedBearerToken; } // 执行Token刷新逻辑 } }
- 预留Token过期缓冲:缓存时将过期时间提前60秒,避免请求过程中Token刚好过期:
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result.ExpiresIn - 60)
内容的提问来源于stack exchange,提问作者Rakesh Kumar
相关产品推荐
相关产品推荐

