应通过redis维护会话拓扑实现精准投递:先查"ws:session:user:10086"定位节点,再定向推送至"ws:node:node-3"专属通道,避免全量广播导致的重复消息。

为什么各节点订阅同一Redis Topic会导致消息重复推送
直接让所有WebSocket节点都 subscribe 到同一个Redis Channel(比如 "ws:topic:order"),看似简单,实则危险——每个节点都会收到完整消息副本,然后各自遍历本地 Session 尝试广播。结果就是:一个订单更新,前端可能收到3次、5次甚至更多次重复消息。
根本原因在于,WebSocket连接是绑定在具体节点上的,而 Session 无法跨JVM序列化或共享。你不能靠“所有节点都收到消息,各自判断要不要推”来解决路由问题。
- 每个节点的
CopyOnWriteArraySet<websocket></websocket>或ConcurrentHashMap<string session></string>只存本机连接 - Redis Pub/Sub 是广播模型,不带目标筛选能力
- 没有会话定位逻辑时,“收到即推”必然造成重复
必须先查会话归属,再定向投递
正确路径不是“发给所有人”,而是“发给那个连着的人”。这意味着每次推送前,得先查 Redis 中维护的映射表,确认目标用户当前连接在哪台节点上。
典型键结构:"ws:session:user:10086" → 值为 "node-2";同时每个节点启动时往 "ws:nodes" 集合里 sadd 自己的标识。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 业务方调用推送接口时,传入
userId或clientId - 服务先
get对应的节点标识,比如得到"node-3" - 再通过内部 RPC、HTTP 调用或 Redis Pub/Sub 的“单播模拟”机制(如用
"ws:node:node-3"作为专属 channel)把消息送过去 - 目标节点查本地
Map找到Session,执行session.getBasicRemote().sendObject()
用 Redisson 的 RTopic 替代原生 Publish/Subscribe
Spring Data Redis 的 RedisMessageListenerContainer 在集群扩缩容时容易丢消息,且不支持对象直传;而 Redisson 的 RTopic 内置序列化、重连、监听器生命周期管理,更适合生产级 WebSocket 集群。
关键点:
- 发送端用
RTopic.publishAsync(obj),自动序列化为 JSON 或 Kryo - 每个节点监听自己专属 channel,例如
"ws:node:" + nodeName,避免全量广播 - 监听器内不做耗时操作,只做
Session查找和异步发送,防止阻塞 Netty 线程 - 别忘了在
@PreDestroy里unsubscribe(),否则节点下线后仍收消息
nginx ip_hash 不是 WebSocket 集群的解药
有人试图用 nginx 的 ip_hash 把同一客户端始终打到同一台后端,以为这样就不用处理会话路由了。这在测试环境可能“看起来能用”,但实际埋了三个雷:
- 客户端 IP 可能被 NAT 或代理覆盖,导致 hash 错乱
- 用户换网络(比如从 WiFi 切到 4G),IP 变了,连接就断在旧节点,新连接进新节点,消息彻底丢失
- 单点故障:某节点宕机,nginx 会切走流量,但旧连接不会自动迁移,已建立的 WebSocket 连接直接中断
真正健壮的方案必须接受“连接随时漂移”这个事实,靠 Redis 维护实时会话拓扑,而不是靠负载均衡策略去固化它。










