WebSocket Session不可序列化,因其绑定TCP连接和容器上下文;正确方案是本地内存存Session+Redis Pub/Sub广播消息意图,各节点按需推送。

Java原生 javax.websocket 的 Session 对象**不能序列化,也不能存入 Redis 或任何外部存储**——这是所有集群方案绕不开的前提。强行序列化会抛 NotSerializableException,或在反序列化后导致连接中断、IO 异常、IllegalStateException: Session is closed 等不可恢复错误。
为什么 WebSocket Session 无法放进 Redis
根本原因在于 Session 是一个运行时绑定底层 TCP 连接和容器线程上下文的“活对象”:它持有 WebSocketContainer、RemoteEndpoint、BasicRemote、SSL session、异步回调队列等非 transient 且不可序列化的字段。即使你用 transient 修饰绕过序列化检查,反序列化后的对象也完全失去网络通道能力,调用 session.getBasicRemote().sendText() 必然失败。
-
Session不是 POJO,它是容器(如 Tomcat、Jetty)管理的有状态资源 - Redis 存的是“谁在线”,不是“Session 本身”;真正发消息时必须依赖本地内存中的
Session实例 - 所有“把 Session 存 Redis”的教程,实际都悄悄用了 Spring 封装的
StandardWebSocketSession或自定义代理,但底层仍依赖本地 session 活着
用 Redis Pub/Sub 实现跨节点消息路由
正确做法是:每个节点只管自己内存里的 Session,靠 Redis 发布「消息意图」,各节点订阅后自行判断“我本地有没有这个用户”,有则推送,无则丢弃。
- 发送方不关心目标 session 在哪台机器,只往 Redis channel(如
ws:msg:room:1001)发 JSON 消息体,含userId、roomId、payload - 所有 WebSocket 节点启动时,用
RedisMessageListenerContainer订阅该 channel - 监听器收到消息后,查本地缓存(如
ConcurrentHashMap<String, Session>)是否有对应userId的Session - 查到就调用
session.getAsyncRemote().sendText(payload);没查到直接 return —— 不报错、不重试、不转发
关键代码片段:
立即学习“Java免费学习笔记(深入)”;
// 订阅监听器示例
public class WsMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String json = new String(message.getBody());
WsMessage msg = JsonUtil.fromJson(json, WsMessage.class);
Session session = WebSocketPool.getSesssionByUserId(msg.getUserId());
if (session != null && session.isOpen()) {
session.getAsyncRemote().sendText(msg.getPayload());
}
}
}
本地 Session 缓存必须用线程安全结构 + 及时清理
别用 HashMap 或静态 ArrayList,否则高并发下必然出现 ConcurrentModificationException 或内存泄漏。更危险的是忘记在 @OnClose 中移除 —— 用户断连后 session 仍驻留内存,ConcurrentHashMap 无限膨胀。
- 必须用
ConcurrentHashMap<String, Session>,key 建议用业务 ID(如userId),而非session.getId()(无业务语义,且重启后失效) -
@OnClose方法里要同步调用map.remove(userId),并做null和isOpen()双重校验 - 加个定时任务每 5 分钟扫描一次过期 session(比如 30 分钟无心跳),避免因网络闪断未触发
@OnClose导致脏数据残留
别忽略心跳与连接粘性对架构的影响
即使用了 Redis Pub/Sub,如果负载均衡器(Nginx / ALB)没开启 sticky session,用户反复重连可能落在不同节点,导致部分消息“发得出去,收不到”。这不是代码 bug,而是部署缺陷。
- Nginx 配置需启用
ip_hash或sticky cookie,确保同一用户长期落在同一台后端 - 前端必须实现心跳(如每 25 秒发
ping),服务端@OnMessage收到后更新本地缓存中该userId的最后活跃时间 - Redis channel 名称建议带环境前缀,如
prod:ws:msg:room:{id},避免测试/预发/生产环境消息互相干扰
最易被忽略的一点:Redis Pub/Sub 是“fire and forget”,不保证送达。如果你的业务要求强一致(比如金融类指令),必须换 Kafka/RocketMQ 并启用 ACK 机制;而大多数聊天、通知类场景,用 Redis 完全够用——但得接受极低概率的丢失,并在前端做超时重发兜底。


















