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
相关产品推荐
相关产品推荐

