在线人数统计必须结合心跳保活与外部存储(如Redis),仅依赖@OnOpen/@OnClose事件易因异常断网、浏览器刷新等导致假在线;ConcurrentHashMap应存userId与心跳时间戳,配合定时清理超时连接,分布式场景下必须用Redis保证全局一致性。

在线人数统计本身不依赖连接池,但高并发下必须用连接池兜底;否则 CopyOnWriteArraySet 或 ConcurrentHashMap 会因连接频繁创建/销毁导致 GC 压力陡增、计数不准。
为什么不能只靠 onOpen/onClose 计数
单纯监听 @OnOpen 和 @OnClose 事件在真实场景中极易漏数或重复计数:
- 客户端异常断网(无
@OnClose触发),连接对象滞留在内存但已不可用 - 浏览器刷新、标签页崩溃、网络抖动都会造成“假在线”
- Spring Boot 的
SimpleBroker模式下,STOMP 连接生命周期与底层 WebSocket 不完全对齐,Session对象可能延迟销毁 - 多个服务实例时,单机内存计数无法反映全局在线量
在线人数统计的可靠落地方式
核心是「连接状态 + 心跳保活 + 外部存储」三者联动,不是靠事件钩子凑数:
- 使用
ConcurrentHashMap存储userId→ 最后心跳时间戳,而非连接对象本身 - 配合定时任务(如每 10 秒扫描)剔除超时(例如 30 秒未心跳)的 key,再更新总数
- 分布式部署时,必须用 Redis 的
SET或HyperLogLog:调用redisTemplate.opsForSet().add("online:room:1001", userId),再用redisTemplate.opsForSet().size("online:room:1001") - 前端建立连接后,必须立即发送一次认证消息(含
userId和roomId),服务端验证通过才写入在线集合,避免未登录连接占位
WebSocket 连接池不是拿来复用“用户连接”的
这是最容易误解的一点:sync.Pool(Go)或 Netty PooledByteBufAllocator(Java)管理的是底层 ByteBuffer、Channel 或 bytes.Buffer,不是业务层的“某个用户的 WebSocket Session”。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 用户级连接不能池化——每个
Session绑定唯一userId、权限、心跳上下文,复用等于串号 - 真正该池化的:HTTP 客户端连接(用于回调鉴权)、JSON 序列化缓冲区、心跳检测线程池、消息编码器实例
- 如果你在 Spring 中看到
WebSocketSession被塞进ConcurrentHashMap,那叫“连接注册表”,不是连接池;它的清理必须依赖@OnClose+ 心跳超时双重机制 - 连接泄漏的典型表现:
java.lang.OutOfMemoryError: unable to create new native thread,根源常是未关闭的Session持有ExecutorService或未注销的ScheduledFuture
连接池配置和心跳的关键参数
不设超时、不配心跳的连接池,上线三天必崩:
-
maxIdleTime(Netty)或idleTimeout(Jetty)必须显式设置,建议 ≤ 60 秒;否则空闲连接永远不释放 - 心跳间隔要小于负载均衡器(如 Nginx)的
proxy_read_timeout,常见组合是:服务端发心跳 25s,Nginx 设为 30s,客户端响应超时设为 20s - Spring Boot 中若用
@EnableWebSocketMessageBroker,需额外配置stompClient.setHeartbeat(10000, 10000),否则 STOMP 层默认不发心跳 - 禁止把
WebSocketSession存入ThreadLocal—— WebSocket 是异步 I/O,线程不固定,会导致内存泄漏和状态错乱
真正的难点不在“怎么加一减一”,而在于怎么定义“在线”:是 TCP 连通?WebSocket OPEN?还是用户最近一次有效业务消息?这个定义一旦模糊,所有统计和禁言逻辑都会失准。










