SemaphoreSlim未按预期工作:多线程并发仍生成多个Token
问题分析与解决方法
你的核心问题在于SemaphoreSlim未实现全局同步,同时缺少内存级的Token缓存机制,导致并发请求绕过同步逻辑重复生成Token。以下是具体原因和修复方案:
核心问题点
- SemaphoreSlim作用域错误:如果
Services类在ASP.NET Core中注册为Scoped(默认行为),每个请求会创建一个独立的Services实例,每个实例的_semaphore都是局部的,无法跨请求同步,多个请求可以同时进入Token生成逻辑。 - 无内存缓存校验:仅依赖数据库查询判断Token有效性,并发请求可能同时查询到"无有效Token"的结果,进而触发多次Token生成。
修复方案
1. 全局化SemaphoreSlim
将_semaphore改为静态字段,确保所有Services实例共用同一个信号量,实现全局同步:
public class Services { // ... 其他字段不变 // 改为静态字段,全局唯一 private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 新增内存缓存Token和过期时间 private static string _currentToken; private static DateTime _tokenExpiry; // ... 构造函数不变 }
2. 优化Token获取逻辑,增加内存缓存校验
在GetAccessTokenAsync中,先快速检查内存缓存(无需等待锁),进入锁后再次校验(避免等待期间Token已被更新),最后再查数据库或生成新Token:
private async Task<string> GetAccessTokenAsync() { // 第一步:快速检查内存缓存,减少锁竞争 if (!string.IsNullOrEmpty(_currentToken) && DateTime.UtcNow < _tokenExpiry) { return _currentToken; } await _semaphore.WaitAsync(); try { // 第二步:进入锁后再次检查,防止等待期间Token已被其他线程更新 if (!string.IsNullOrEmpty(_currentToken) && DateTime.UtcNow < _tokenExpiry) { return _currentToken; } // 第三步:检查数据库中的有效Token var dbValidToken = await GetValidTokenFromDatabaseAsync(); if (dbValidToken != null && DateTime.UtcNow < dbValidToken.ExpiryTime.AddMinutes(-1)) { // 更新内存缓存,提前1分钟过期避免临界时间点问题 _currentToken = dbValidToken.Token; _tokenExpiry = dbValidToken.ExpiryTime.AddMinutes(-1); return _currentToken; } // 第四步:生成新Token并更新缓存和数据库 var newToken = await RequestAccessTokenAsync(); await SaveNewTokenToDatabaseAsync(newToken); _currentToken = newToken.AccessToken; // 提前1分钟设置过期时间,预留缓冲 _tokenExpiry = newToken.ExpiryTime.AddMinutes(-1); return _currentToken; } finally { _semaphore.Release(); } } // 补充数据库操作的占位方法 private async Task<DbTokenModel> GetValidTokenFromDatabaseAsync() { // 实现查询数据库中未过期Token的逻辑 return null; } private async Task SaveNewTokenToDatabaseAsync(TokenResponse newToken) { // 实现将新Token保存到数据库的逻辑,可同时清理过期Token }
3. 调整Services的注册生命周期
在ASP.NET Core的Program.cs中,将Services注册为Singleton,减少实例创建开销并确保逻辑一致性:
builder.Services.AddSingleton<Services>();
关键优化说明
- 双重校验锁:内存缓存的两次检查,既减少了锁竞争,又避免了等待锁期间Token已被更新的问题。
- 过期缓冲:Token过期时间提前1分钟设置,避免刚好在Token过期瞬间的并发请求重复生成Token。
- 全局信号量:静态
SemaphoreSlim确保同一时间只有一个线程能进入Token生成逻辑。
内容的提问来源于stack exchange,提问作者StealthGhost
相关产品推荐
相关产品推荐

