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>();
目前该实现运行正常,但我想确认这是否为正确的线程安全单例实现,是否无需额外锁机制?
核心问题分析
Token单例实例创建的线程安全性
使用Lazy<Token>默认模式(LazyThreadSafetyMode.ExecutionAndPublication)创建单例是线程安全的,只会初始化一次Token实例,这部分没问题。但Token类的TokenValue和ExpiresOn是公共可读写属性,多线程环境下可能出现读写不一致:比如线程A在执行HasTokenExpired()读取属性时,线程B正在更新这两个属性,导致A读到的是半更新状态的数据。令牌刷新的并发问题
当前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
相关产品推荐
相关产品推荐

