因为WebSocketSession绑定单个JVM的TCP连接,无法序列化和跨节点传输,导致遍历本地ConcurrentHashMap时看不到其他节点用户,出现消息仅本节点接收或目标用户离线等失败现象。

为什么直接用 WebSocketSession 广播会失败
因为 WebSocketSession 是绑定在单个 JVM 进程里的 TCP 连接对象,无法序列化、不能跨节点传输。你在一个服务节点里遍历 ConcurrentHashMap<userid websocketsession></userid>,永远看不到其他节点上用户的连接。
常见错误现象包括:管理员发通知,只有当前节点的用户收到;A 用户发消息给 B,B 恰好连在另一台机器上,消息就“消失”了。
- 别试图把
WebSocketSession存 Redis(它不是 POJO,序列化会失败) - 别用 Spring 的
@SendToUser跨节点广播(它只作用于本机 Session) - 不要依赖
STOMP的 broker 代理自动路由(默认内存 broker 不跨节点)
Redis Pub/Sub 是最轻量可靠的同步机制
Redis 的发布/订阅模型天然适合做集群间消息中转:低延迟、无状态、不落盘、所有订阅者实时收消息。关键不是“存”,而是“广播”。
每个 WebSocket 服务节点都启动一个独立的订阅者,监听同一个频道(如 ws:global:msg),收到消息后只负责转发给本机已连接的用户。
- 频道名建议带命名空间前缀,避免不同环境或服务冲突,例如
ws:prod:notify - 不要用 Redis 的
KEYS或SCAN查在线用户——Pub/Sub 本身不维护状态,状态仍由各节点本地 Map 管理 - 若需按用户分发(非广播),可在消息体里加
targetUserId字段,本节点收到后查自己 Map 是否有该用户
Spring Boot 中怎么写发布和订阅逻辑
别用原生 Jedis 手动建 PubSub,Spring Data Redis 提供了 RedisTemplate 和 MessageListener 封装,更稳。
发布很简单:redisTemplate.convertAndSend("ws:global:msg", jsonPayload);订阅需注册一个监听器,注意监听器要随应用生命周期启停。
- 监听器 Bean 必须实现
MessageListener接口,并在容器启动时注册到RedisMessageListenerContainer - 消息体推荐用标准 JSON 字符串,字段至少包含
type、target、data,方便多业务复用 - 反序列化失败时,
MessageListener默认静默丢弃,务必加 try-catch + 日志,否则消息丢失无迹可寻
集群部署时最容易被忽略的三个点
Redis 地址必须指向同一套实例(单节点 or Redis Cluster),所有服务节点共用一个 Pub/Sub 上下文。如果各节点连的是不同 Redis,频道根本不通。
-
RedisMessageListenerContainer的线程池大小默认是 1,高并发下会积压,建议设为 CPU 核数 × 2 - Spring Boot 默认不开启 Redis 订阅功能,需显式配置
@EnableRedisHttpSession不起作用,真正需要的是RedisMessageListenerContainerBean - 本地开发时容易用
localhost:6379,上线后必须替换成真实 Redis 地址,K8s 环境尤其注意 Service DNS 名是否可达
真正的难点不在代码,而在确认所有节点是否真的连到了同一个 Redis 实例、是否都成功 subscribe 到了目标频道——用 redis-cli --csv pubsub channels "ws:*" 和 pubsub numsub "ws:global:msg" 实时验证最靠谱。


















