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

ASP.NET Core 6 Web API线程安全单例复用访问令牌实现问询

令牌复用实现的线程安全性分析

我在ASP.NET Core 6 Web API中调用另一个API B获取访问令牌,希望复用该令牌直至过期以多次调用B。我编写了如下Token单例类、TokenResource类,并在Startup中将TokenResource注册为单例:

public sealed class Token
{
    private static readonly Lazy<Token> lazy =
   new Lazy<Token>(() => new Token());

    public static Token Instance { get { return lazy.Value; } }
   
    public Token()
    {
    } 
 
    public string TokenValue { get; set; }
    public DateTime ExpiresOn { get; set; }

    public bool HasTokenExpired()
    { 
      if (string.IsNullOrEmpty(TokenValue))
      {
        return true;
      }

      if (DateTime.UtcNow >= ExpiresOn.AddSeconds(-20))
      {
        return true;
       }
        return false;           
    }
}
public class TokenResource : ITokenResource
{
    private static Token Token
    {
      get
      {
         return Token.Instance;
      }
     }

    public async Task<string> GetTokenAsync()
    {
        if (Token.HasTokenExpired())
        {
            await RefreshTokenAsync();
        }            
        return Token.TokenValue;                
    }  

    public async Task RefreshTokenAsync()
    {
         // 发送HTTP请求获取令牌
         var tokenString = JObject.Parse(tokenResponseAsString)["access_token"].Value<string>();            
         Token.TokenValue = tokenString;
         Token.ExpiresOn = SetTokenExpirationDate(tokenString);            
    }
}
builder.Services.AddSingleton<ITokenResource, TokenResource>();

目前该实现运行正常,但我想确认这是否为正确的线程安全单例实现,是否无需额外锁机制?


核心问题分析

  1. Token单例实例创建的线程安全性
    使用Lazy<Token>默认模式(LazyThreadSafetyMode.ExecutionAndPublication)创建单例是线程安全的,只会初始化一次Token实例,这部分没问题。但Token类的TokenValue和ExpiresOn是公共可读写属性,多线程环境下可能出现读写不一致:比如线程A在执行HasTokenExpired()读取属性时,线程B正在更新这两个属性,导致A读到的是半更新状态的数据。

  2. 令牌刷新的并发问题
    当前GetTokenAsync和RefreshTokenAsync没有同步机制,当多个线程同时检测到令牌过期时,会同时触发多次令牌刷新请求,不仅浪费API B的资源,还可能出现后获取的令牌覆盖先获取的令牌(虽然过期时间差异不大,但属于不必要的并发问题)。

改进方案

需要给令牌刷新逻辑添加异步锁,确保同一时间只有一个线程去刷新令牌,同时保证属性读写的一致性。推荐使用SemaphoreSlim实现异步锁:

public class TokenResource : ITokenResource
{
    private static readonly Token _token = Token.Instance;
    // 初始化信号量,允许1个线程进入
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task<string> GetTokenAsync()
    {
        if (_token.HasTokenExpired())
        {
            await _semaphore.WaitAsync();
            try
            {
                // 二次检查:防止等待期间已有其他线程刷新了令牌
                if (_token.HasTokenExpired())
                {
                    await RefreshTokenAsync();
                }
            }
            finally
            {
                _semaphore.Release();
            }
        }            
        return _token.TokenValue;                
    }  

    private async Task RefreshTokenAsync()
    {
         // 发送HTTP请求获取令牌
         var tokenResponse = await YourHttpClient.PostAsync(...);
         var tokenResponseAsString = await tokenResponse.Content.ReadAsStringAsync();
         var tokenString = JObject.Parse(tokenResponseAsString)["access_token"].Value<string>();            
         _token.TokenValue = tokenString;
         _token.ExpiresOn = SetTokenExpirationDate(tokenString);            
    }
}

补充说明

  • 二次检查令牌是否过期很重要:因为当多个线程等待信号量时,第一个线程已经完成了刷新,后续线程无需再重复刷新。
  • SemaphoreSlim适合异步场景,比传统的lock更友好,不会阻塞线程池线程。
  • Token类的属性可以考虑改为私有字段加只读属性,只通过RefreshTokenAsync更新,减少外部意外修改的风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 21:23:19