如何在ASP.NET Core中低开销跟踪用户最后活跃时间?
解决方案:无需每次请求更新数据库的用户最后活跃时间跟踪
针对你的需求,这里有几种兼顾精度和性能的方案,避免每次请求都写入数据库:
方案1:利用滑动身份认证令牌的刷新时机更新
ASP.NET Core Identity的滑动登录机制只会在令牌接近过期时才刷新Cookie(当剩余有效期小于滑动过期窗口时长时触发),我们可以在这个时机更新用户的最后活跃时间,仅在需要刷新令牌时触发数据库操作,大幅减少写入次数。
实现方式是通过CookieAuthenticationEvents的OnValidatePrincipal事件:
services.ConfigureApplicationCookie(options => { options.Events.OnValidatePrincipal = async context => { // 检查是否需要刷新滑动令牌 if (context.ShouldRefresh) { var userId = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); if (!string.IsNullOrEmpty(userId)) { var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>(); var user = await userManager.FindByIdAsync(userId); if (user != null) { user.LastActive = DateTime.UtcNow; await userManager.UpdateAsync(user); } } } }; });
优点:
- 实现简单,无需额外组件
- 数据库操作次数少,仅在令牌刷新时触发
- 与Identity的滑动机制天然契合,最后活跃时间能反映用户最近的有效访问
缺点:
- 精度取决于滑动过期窗口设置,误差范围等于窗口时长
方案2:缓存+后台批量同步
将用户的最后活跃时间先写入内存/分布式缓存(内存操作开销极低),再通过后台定时任务批量同步到数据库。这种方式完全避免了请求链路中的数据库写入,性能最优。
步骤1:中间件更新缓存
每次用户访问已认证页面时,更新缓存中的最后活跃时间(设置合理的更新间隔,避免频繁写缓存):
app.Use(async (context, next) => { if (context.User.Identity.IsAuthenticated) { var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); var cacheKey = $"UserLastActive:{userId}"; var cache = context.RequestServices.GetRequiredService<IDistributedCache>(); var cachedTimeStr = await cache.GetStringAsync(cacheKey); var now = DateTime.UtcNow; // 只有当缓存不存在,或距离上次更新超过1小时才更新 if (cachedTimeStr == null || DateTime.Parse(cachedTimeStr) < now.AddHours(-1)) { await cache.SetStringAsync( cacheKey, now.ToString(), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) } ); } } await next(); });
步骤2:后台服务批量同步到数据库
创建一个BackgroundService,定期扫描缓存中的活跃时间并批量更新到数据库:
public class LastActiveSyncService : BackgroundService { private readonly IDistributedCache _cache; private readonly UserManager<IdentityUser> _userManager; private readonly ILogger<LastActiveSyncService> _logger; public LastActiveSyncService(IDistributedCache cache, UserManager<IdentityUser> userManager, ILogger<LastActiveSyncService> logger) { _cache = cache; _userManager = userManager; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { // 以Redis为例,不同缓存实现的键扫描方式需调整 var multiplexer = _cache.GetService<IConnectionMultiplexer>(); var server = multiplexer.GetServer(multiplexer.GetEndPoints().First()); var keys = server.Keys(pattern: "UserLastActive:*").Select(k => k.ToString()); foreach (var key in keys) { var userId = key.Replace("UserLastActive:", ""); var cachedTimeStr = await _cache.GetStringAsync(key); if (DateTime.TryParse(cachedTimeStr, out var lastActive)) { var user = await _userManager.FindByIdAsync(userId); if (user != null && user.LastActive < lastActive) { user.LastActive = lastActive; await _userManager.UpdateAsync(user); } // 同步后移除缓存,避免重复处理 await _cache.RemoveAsync(key); } } } catch (Exception ex) { _logger.LogError(ex, "批量同步用户最后活跃时间失败"); } // 每小时执行一次同步 await Task.Delay(TimeSpan.FromHours(1), stoppingToken); } } }
步骤3:注册后台服务
services.AddHostedService<LastActiveSyncService>();
优点:
- 请求链路中无数据库操作,性能开销极小
- 批量更新进一步降低数据库压力
- 缓存更新间隔可灵活调整精度
缺点:
- 实现相对复杂,需要依赖缓存组件
- 若后台服务故障,可能会丢失部分活跃数据(可通过延长缓存过期时间缓解)
关于早期ASP.NET的会话事件替代
ASP.NET Core中没有直接的Session_Start事件,但可以通过中间件检测Session是否为新创建的:
app.UseSession(); app.Use(async (context, next) => { if (context.User.Identity.IsAuthenticated && context.Session.IsAvailable && !context.Session.TryGetValue("SessionInitialized", out _)) { // 标记会话已初始化 context.Session.SetString("SessionInitialized", DateTime.UtcNow.ToString()); // 可结合前面的方案更新活跃时间 } await next(); });
不过这种方式依赖Session可用性,且无法覆盖同一会话内的多次访问,更适合作为前面方案的补充。
内容的提问来源于stack exchange,提问作者Jonathan Wood
相关产品推荐
相关产品推荐

