核心是Redis Pub/Sub消息中转,Session必须本地存储;因WebSocketSession不可序列化,存Redis会抛NotSerializableException,反序列化后IO通道失效,导致消息发送失败或连接丢失。

跨服务器推送 WebSocket 消息,核心不是“把 Session 存 Redis”,而是用 Redis Pub/Sub 做消息中转——Session 必须留在本地内存,否则会触发序列化失败、线程不安全、心跳超时等一连串问题。
为什么不能把 WebSocketSession 存进 Redis
因为 WebSocketSession 是与 Servlet 容器强绑定的运行时对象(含 NativeWebSocketSession、StandardWebSocketSession 等具体实现),不可序列化,强行存 Redis 会抛 NotSerializableException 或在反序列化后变成无效句柄。即使绕过序列化(如用 Redisson 的 RMapCache),也无法调用 session.sendMessage() ——底层 IO 通道已关闭或不属于当前 JVM 实例。
常见错误现象:
- 应用启动时报
java.io.NotSerializableException: org.springframework.web.socket.adapter.standard.StandardWebSocketSession - 消息看似“发成功”,但前端收不到,日志里出现
IllegalStateException: The session has been closed and no longer available - 集群节点间 Session ID 冲突,导致 A 节点误删 B 节点的连接
RedisMessageListenerContainer 必须手动注册监听器
Spring Boot 默认不自动订阅任何频道,仅靠 @Configuration + RedisMessageListenerContainer bean 不足以让服务收到消息。必须显式调用 addMessageListener() 并指定频道(或 PatternTopic)。
实操要点:
- 监听器需实现
MessageListener接口,onMessage()中解析message.getChannel()和message.getBody() - 推荐用
PatternTopic("ws:topic:*")支持按用户/群组动态订阅,避免硬编码频道名 - 不要在监听器里直接操作 Spring 上下文 Bean(如
SimpMessagingTemplate),应通过@Autowired注入或使用ApplicationContext.getBean() - 监听器方法内务必加 try-catch,否则异常会导致整个 listener 停摆,后续消息全部丢失
用户路由映射必须写对 CACHE_KEY 格式
跨节点寻址依赖 Redis 中的用户-节点映射关系,格式错一个字符(比如多空格、少冒号、大小写混用)就会查不到目标实例,消息静默丢弃。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
典型正确写法:
redisTemplate.opsForSet().add("ws:user:" + userId, serverId + ":" + sessionId);
关键约束:
-
serverId建议用启动时生成的 UUID,不要用 hostname 或 IP(容器环境可能重复) -
sessionId必须取自WebSocketSession.getId(),不是 HTTP Session ID - 删除逻辑要成对:连接关闭时,既清本地
ConcurrentHashMap,也执行redisTemplate.opsForSet().remove("ws:user:" + userId, serverId + ":" + sessionId) - 建议加 TTL(如
expire("ws:user:" + userId, 24, TimeUnit.HOURS)),防僵尸映射累积
STOMP over WebSocket 下,SimpMessagingTemplate 只推本机
很多人误以为调用 simpMessagingTemplate.convertAndSendToUser() 就能跨节点推送——实际它只广播到当前 JVM 内的 STOMP 订阅者。集群场景下,必须自己封装一层:先查 Redis 获取目标用户所在节点列表,再通过 Pub/Sub 把消息发到对应频道,由目标节点本地消费并调用 SimpMessagingTemplate。
容易忽略的细节:
- STOMP 的
/user/queue路径是 Spring Security 自动绑定的,不适用于跨节点;应统一走自定义频道如ws:user:{userId} - 消息体建议用 JSON 字符串,字段至少包含
{"type":"msg","userId":"U1001","content":"..."},避免类型混淆 - 如果用了 Spring Security 的 WebSocket 认证,
Principal在 Pub/Sub 消费端不可用,需在消息体里透传必要上下文(如权限标识)
最易被跳过的环节是连接清理:没配定时任务扫描过期连接,也没监听 afterConnectionClosed 做兜底删除,跑一周后 Redis 里全是失效的 serverId:sessionId 映射,新消息永远找不到接收方。

















