You need to enable JavaScript to run this app. ASP.NET Core SignalR如何实现延迟5分钟检测掉线用户重连的定时任务
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core SignalR如何实现延迟5分钟检测掉线用户重连的定时任务

你的现有写法评估

你的await Task.Delay(5分钟)写法没有直接安全风险,但属于不良实践,核心问题有两个:

  • SignalR的Hub是瞬态实例,每次连接请求创建、方法调用结束后就会被回收,你在OnDisconnectedAsync中持有5分钟的等待逻辑,会导致该Hub关联的上下文、用户状态等资源无法被GC及时回收,高并发场景下大量掉线用户会产生大量挂起的延迟任务,带来不必要的内存开销。
  • 当服务触发优雅重启/关闭时,这些未完成的Task.Delay会直接被取消,后续清理逻辑不会执行,即使你忽略服务器崩溃的场景,正常运维的重启操作也会受影响。

更合理的实现方案

完全不需要引入第三方库,也不用复杂的CronJob,用ASP.NET Core原生自带的BackgroundService(托管后台服务)就能实现,整体逻辑非常轻量:

步骤1:改造内存存储结构

给掉线用户表增加DroppedAt字段,记录用户进入掉线表的时间,同时封装线程安全的会话管理服务(因为多连接和后台服务会并发操作内存数据,必须加锁):

// 会话管理接口,注册为单例
public interface IUserSessionManager
{
    // 用户主动退出时直接清理
    void RemoveOnlineUser(Guid userId);
    // 意外掉线时加入掉线表
    void AddDroppedUser(User user);
    // 用户重连时从掉线表移除
    void RemoveDroppedUser(Guid userId);
    // 获取超过5分钟未重连的掉线用户
    List<User> GetExpiredDroppedUsers(TimeSpan expireAfter);
    // 执行最终下线逻辑
    void DoDisconnectUser(User user);
}

// 内存实现类
public class InMemoryUserSessionManager : IUserSessionManager
{
    private readonly object _lock = new();
    private readonly List<User> _onlineUsers = [];
    private readonly List<(User User, DateTime DroppedAt)> _droppedUsers = [];

    public void AddDroppedUser(User user)
    {
        lock (_lock)
        {
            _droppedUsers.Add((user, DateTime.UtcNow));
            _onlineUsers.RemoveAll(u => u.Id == user.Id);
        }
    }

    public List<User> GetExpiredDroppedUsers(TimeSpan expireAfter)
    {
        lock (_lock)
        {
            var cutoffTime = DateTime.UtcNow - expireAfter;
            return _droppedUsers
                .Where(x => x.DroppedAt <= cutoffTime)
                .Select(x => x.User)
                .ToList();
        }
    }

    // 其他接口方法按上述加锁逻辑实现即可
}

步骤2:实现轻量后台清理服务

继承原生BackgroundService实现定时扫描逻辑,比如每1分钟扫一次,清理超过5分钟未重连的用户:

public class DroppedUserCleanupService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    // 扫描间隔,可自行调整,不需要高精度的话设1-2分钟都可以
    private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
    // 掉线超时时间,对应你的5分钟需求
    private readonly TimeSpan _droppedTimeout = TimeSpan.FromMinutes(5);

    public DroppedUserCleanupService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var sessionManager = scope.ServiceProvider.GetRequiredService<IUserSessionManager>();
            
            // 拿到所有超时未重连的用户
            var expiredUsers = sessionManager.GetExpiredDroppedUsers(_droppedTimeout);
            foreach (var user in expiredUsers)
            {
                sessionManager.DoDisconnectUser(user);
                sessionManager.RemoveDroppedUser(user.Id);
            }

            await Task.Delay(_checkInterval, stoppingToken);
        }
    }
}

步骤3:注册服务

在Program.cs中注册上述两个服务即可:

// 会话管理注册为单例,全局共享内存数据
builder.Services.AddSingleton<IUserSessionManager, InMemoryUserSessionManager>();
// 注册后台清理服务
builder.Services.AddHostedService<DroppedUserCleanupService>();

步骤4:调整Hub逻辑

你原有的Hub逻辑基本不用改,只需要删除OnDisconnectedAsync里的Task.Delay相关代码,改成调用IUserSessionManager的对应方法即可:

  • OnConnectedAsync:用户连接时调用RemoveDroppedUser,再将用户加入在线表
  • EndSessionAsync:用户主动退出时调用RemoveOnlineUser,直接清理,不需要进掉线表
  • OnDisconnectedAsync:判断如果用户没有主动调用过EndSessionAsync,就调用AddDroppedUser加入掉线表即可

这个方案所有依赖都是ASP.NET Core基础框架自带的,没有额外引入第三方组件,逻辑复杂度很低,也避免了你原来写法的资源泄漏问题,性能和稳定性都更好。


内容的提问来源于stack exchange,提问作者AsPas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 22:03:03