前言
伴随着ASP.NET Zero系统日渐运行,通过/api/TokenAuth/Authenticate获取Token的速度会逐渐变慢,到最后会呈现出一次获取会超过20秒或者导致超时的现象。
先说结论
导致问题产生的代码:
TokenAuthController > CreateJwtClaims > AddTokenValidityKeyAsync
await _userManager.AddTokenValidityKeyAsync(
user,
tokenValidityKey,
expirationDate
);
问题
为什么一个获取Token的接口会出现如此性能问题呢?究竟是什么情况下会产生这样的问题?带着这样的疑问,开始了以下的debug之路。
一、调试Authenticate
-
通过postman调用/api/TokenAuth/Authenticate
-
从调用结果看到,我们获取Token的总耗时是2.20s。
-
通过debug接口可以发现接口/api/TokenAuth/Authenticate的主要耗时点在CreateAccessToken,总共花费了2.02s
-
这段代码是在做什么呢?通过代码分析,我们可以知道他总共做了2件事情。第一件事情就是CreateJwtClaims,第二件事情就是通过JwtClaims创建JwtSecurityToken
-
再次对CreateAccessToken&CreateJwtClaims进行调试,从以下截图我们可以知道。罪魁祸首就是AddTokenValidityKeyAsync,基本的耗时都在这里。
-
通过ILSpy工具观看源码
//AbpUserManager.cs public virtual async Task RemoveTokenValidityKeyAsync(TUser user, string tokenValidityKey, CancellationToken cancellationToken = default(CancellationToken)) { await AbpUserStore.RemoveTokenValidityKeyAsync(user, tokenValidityKey, cancellationToken); }
//AbpUserStore.cs public virtual async Task AddTokenValidityKeyAsync(TUser user, string tokenValidityKey, DateTime expireDate, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); Check.NotNull<TUser>(user, "user"); await RepositoryExtensions.EnsureCollectionLoadedAsync<TUser, long, UserToken>(UserRepository, user, (Expression<Func<TUser, IEnumerable<UserToken>>>)((TUser u) => u.Tokens), cancellationToken); user.Tokens.Add(new UserToken((AbpUserBase)(object)user, "TokenValidityKeyProvider", tokenValidityKey, null, expireDate)); }
-
其中await RepositoryExtensions.EnsureCollectionLoadedAsync会加载所有导航实体,此操作会随着AbpUserTokens表的增大而耗费的时间加长。
二、解决问题
-
方法一:通过sql server 直接删除 AbpUserTokens 表数据
--这区间内的数据是一年后过期的RefreshToken数据,AccessToken的失效日期是一天,基本不需要处理。 DECLARE @dtnow datetime = getdate() DECLARE @BeginTime datetime = DATEADD(day, 2, @dtnow) DECLARE @EndTime datetime = DATEADD(year, 1, @dtnow) DELETE FROM AbpUserTokens WHERE Id IN (SELECT Id FROM AbpUserTokens WHERE ExpireDate > @BeginTime AND ExpireDate <= @EndTime)
-
方法二:通过ASP.NET Zero 框架中的 background job定时删除
public class UserTokenExpirationWorker : PeriodicBackgroundWorkerBase, ISingletonDependency { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IRepository<UserToken, long> _userTokenRepository; private readonly IRepository<Tenant> _tenantRepository; public UserTokenExpirationWorker( IUnitOfWorkManager unitOfWorkManager, IRepository<UserToken, long> userTokenRepository, IRepository<Tenant> tenantRepository, AbpTimer timer) : base(timer) { _userTokenRepository = userTokenRepository; _unitOfWorkManager = unitOfWorkManager; _tenantRepository = tenantRepository; Timer.Period = 1 * 1000 * 60; // 执行间隔时间; } [UnitOfWork] protected override void DoWork() { List<int> tenantIds; var utcNow = Clock.Now.ToUniversalTime(); //由于AccessToken有效期是1天有效期,故此处是当前时间+2天。区间和范围都可以根据自己实际情况调整 var beginTime = utcNow.AddDays(2); var endTime = utcNow.AddYears(1); using (var uow = _unitOfWorkManager.Begin()) { using (_unitOfWorkManager.Current.SetTenantId(null)) { _userTokenRepository.Delete(t => t.ExpireDate > beginTime && t.ExpireDate <= endTime); tenantIds = _tenantRepository.GetAll().Select(t => t.Id).ToList(); uow.Complete(); } } foreach (var tenantId in tenantIds) { using (var uow = _unitOfWorkManager.Begin()) { using (_unitOfWorkManager.Current.SetTenantId(tenantId)) { _userTokenRepository.Delete(t => t.ExpireDate <= beginTime && t.ExpireDate <= endTime); uow.Complete(); } } } } }
-
执行完以上2种方法后再次调用接口/api/TokenAuth/Authenticate,仅耗时54ms,获取速度回归到正常水平。