WebSocket Session 不能存 Redis 是因其实现未序列化且绑定 TCP 连接与容器生命周期;正确做法是本地内存存会话 + Redis Pub/Sub 广播消息。

WebSocket Session 不能存 Redis,强行序列化会抛 NotSerializableException 或运行时连接中断——这不是配置问题,是设计限制:它绑定了底层 TCP 连接、线程上下文和 Servlet 容器生命周期,跨 JVM 无法重建。
为什么 WebSocket Session 不能直接放进 Redis
根本原因不是 Redis 不支持,而是 WebSocketSession 类本身没实现 java.io.Serializable,且内部持有 javax.servlet.http.HttpServletRequest、AsyncContext、NativeWebSocketSession 等不可序列化对象。即使用 JdkSerializationRedisSerializer 强行存,反序列化后也无法恢复网络状态,推送必然失败。
- 常见错误现象:
java.io.NotSerializableException: org.springframework.web.socket.sockjs.transport.session.SockJsSession - HTTP Session 可序列化,是因为 Tomcat 的
StandardManager做了定制持久化;WebSocket 没有等价机制 - Spring Session 对 WebSocket 无效——它只接管
HttpSession,不碰WebSocketSession
正确做法:本地内存 + Redis Pub/Sub 广播
每个节点只管自己连上的用户,用 ConcurrentHashMap<string websocketsession></string> 存本机会话(key 通常是用户 ID 或设备 ID);发消息时,不查远程 session,而是通过 Redis 频道广播「谁该收什么」,让所有节点自己判断本地有没有对应 session。
- 发送端逻辑:
stringRedisTemplate.convertAndSend("ws:topic:msg", "{\"userId\":\"u1001\",\"content\":\"hello\"}"); - 监听端逻辑:收到消息后解析
userId,查本地sessionMap.get("u1001"),存在则调用session.sendMessage(...) - 必须加空值保护:
if (session != null && session.isOpen()) { ... },避免已断连 session 调用失败 - 频道名建议带环境前缀,如
ws:prod:msg,避免测试/预发/生产混播
RedisMessageListenerContainer 初始化要点
监听器容器必须在 Spring 上下文刷新后才真正订阅,否则启动期间的广播消息会丢失。别用 @PostConstruct 手动注册,交给 Spring 管理生命周期。
- 必须显式设置
container.setPhase(2147483647)(即Integer.MAX_VALUE),确保它最后启动、最早停止 -
PatternTopic比ChannelTopic更灵活,支持通配符,但生产环境建议用精确频道名减少误匹配 - 监听器方法签名必须是
public void onMessage(Message message, byte[] pattern),否则MessageListenerAdapter无法反射调用 - 消息体推荐用 JSON 字符串,不要传 Java 对象——避免各节点类版本不一致导致反序列化失败
上线前必验的三个边界点
集群环境下最易漏掉的是会话映射一致性、消息重复消费、以及节点启停时的状态错位。
- 用户重连时,旧 session 必须从本地 map 中
remove,否则新连接进来后 map 里存两个,广播消息会推两遍 - Redis Pub/Sub 不保证消息不丢也不保证只投一次,业务层需容忍少量重复;若要求严格有序,得换 RocketMQ/Kafka,但代价是引入新组件
- 节点下线前,应主动向 Redis 发布一条
ws:node:offline消息,通知其他节点清理该节点曾注册过的用户路由缓存(如有)
真正的难点不在代码怎么写,而在于所有节点对「谁在线」的理解必须完全基于本地内存——任何试图把 session 当数据同步的思路,都会在第二个节点上线时崩掉。


















