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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 00:54:50