结论:高并发 WebSocket 推送需用业务 ID 绑定、分组预计算、外部路由(如 Redis/Kafka)和异步批处理,禁用全量内存遍历;ConcurrentHashMap 在千级广播时因遍历锁争、缓存伪共享、GC 压力及无集群共享而失效。

直接说结论:一对一靠 Session ID 映射,一对多靠分组标识(如用户ID、房间ID、标签),但高并发下不能硬查内存 Map,必须用异步 + 外部协调机制。
为什么 ConcurrentHashMap.put/get 在高并发推送时会拖垮性能
很多人一上来就写 ConcurrentHashMap<string session></string> 存所有连接,看似线程安全,实际在 10k+ 连接、每秒千级广播时会出问题:
- 每次
clients.values()遍历都是全量锁竞争,CPU cache line false sharing 风险高 -
getAsyncRemote().sendText()调用本身不阻塞,但底层 Netty 的Channel.writeAndFlush()仍需排队进 EventLoop,大量并发 write 会堆积 write buffer - GC 压力陡增:每个
Session持有WebSocketSession、Principal、HandshakeRequest等对象,百万连接 ≈ 数 GB 堆外 + 堆内内存 - 单机扛不住时,Map 无法跨节点共享 —— 这是集群场景下最致命的限制
一对一推送:别只认 session.getId(),要用业务 ID 绑定
session.getId() 是容器生成的 UUID,重启即失效,且无业务语义。真实场景中,你得让客户端在握手时带上身份凭证(如 token 或 userId):
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- Spring WebSocket 可在
HandshakeInterceptor中解析请求参数或 header,把userId存入HttpSession或直接设为session.getUserProperties().put("userId", "U123") - 服务端推送时,查的是
userId → Session映射,不是sessionId → Session - 推荐用
ConcurrentHashMap<string copyonwritearraylist>></string>存储:一个 userId 可能对应多个设备(Web + App + 小程序),需支持多端在线 - 务必加超时清理:用
ScheduledExecutorService定期扫描 lastAccessTime,剔除 5 分钟无心跳的Session
一对多推送:按“逻辑分组”而非“物理连接”组织数据
群聊、订单通知、直播间等场景,本质是“某类事件触发对某组人的广播”,不是“遍历所有在线人”。关键在分组建模:
- 避免用
for (Session s : clients.values())全局扫 —— 改成先查 Redis Set:SMEMBERS group:order:10086,再批量 fetch 对应的Session实例 - 分组键设计要可扩展:比如
group:userTag:vip、group:region:shanghai,便于运营后台动态调整 - 推送前做轻量过滤:比如消息带
"level": "high",而某 Session 标记了mute:true,就跳过它,别等 write 失败才处理 - 异步扇出必须加限流:用
RateLimiter控制每秒最多推 500 条,防止突发流量打满网络栈或触发 TCP 重传
高并发匹配算法:别在内存里算,让 Redis 和消息队列承担路由逻辑
当连接数超 5k、推送 QPS > 200,纯内存结构已到瓶颈。真实生产方案是“三层解耦”:
- 接入层(Netty/Tomcat):只负责连接管理、心跳、认证,把
userId和nodeId(如ws-01)存入 Redis Hash:HSET user:U123 node ws-01 session S12345 - 路由层(Redis Pub/Sub 或 Kafka):发消息时,先 publish 到
topic:order:10086,各节点订阅后,查本地缓存或 Redis 获取目标userId列表 - 推送层(本地 Session 缓存 + 批量 write):每个节点只推自己负责的连接;用
ChannelGroup.writeAndFlush()替代单个Channel.write(),减少 event loop 切换 - 注意:Redis 不是万能的 —— 如果单次广播要推 10w 人,
SMEMBERS返回大数组会阻塞 Redis,改用SSCAN分批 + 异步 pipeline 写入
真正难的不是“怎么发”,而是“怎么确保只发一次、不漏、不重复、不压垮”。业务 ID 绑定、分组预计算、外部路由、异步批处理 —— 这四点漏掉任一环,上线后都会在凌晨三点收到告警电话。

















