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

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,理由如下:

  1. Polly作为专业容错库,提供了重试、熔断、隔离等完善机制,后续扩展其他容错需求成本极低
  2. 职责分离清晰,客户端负责API交互和Token管理,Polly负责重试策略,代码结构更易维护
  3. 调整重试规则只需修改Polly配置,无需改动业务代码

优化建议

  1. 修正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;
}
  1. 添加带抖动的重试延迟:替换0延迟,用指数退避+抖动避免并发压力:
retryAttempt => 
    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) 
    + TimeSpan.FromMilliseconds(_jitterer.Next(0, 1000))
  1. 解决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刷新逻辑
    }
}
  1. 预留Token过期缓冲:缓存时将过期时间提前60秒,避免请求过程中Token刚好过期:
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result.ExpiresIn - 60)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 10:59:50