websocket 怎么做 Springboot 的分布式服务
websocket 怎么做 Springboot 的分布式服务
在 Spring Boot 做 WebSocket 分布式服务 时,核心难点只有一句话:
WebSocket 是“长连接 + 有状态”,而分布式是“多实例 + 无状态”
下面我按 从问题 → 架构 → 实现方案 → 代码示例 → 常见坑,一步一步给你讲清楚。
一、为什么 WebSocket 在分布式下会出问题?
单机 WebSocket 一般是这样:
Client ──WebSocket──> SpringBoot A服务里通常会保存:
sessionId
userId → WebSocketSession 映射
在线用户列表
❌ 分布式后变成这样
┌── SpringBoot A
Client ─LB─┤
└── SpringBoot B问题立刻出现:
- 连接在哪台机器?
- 用户连在 A
- 业务消息却落在 B
- WebSocketSession 不能跨 JVM
- 不能放 Redis
- 不能序列化
- 节点之间不能互相推消息
👉 所以:WebSocket 分布式 ≠ 共享 session
二、WebSocket 分布式的三种主流方案(重点)
✅ 方案一:Sticky Session(最简单,但不推荐)
思路
- 负载均衡保证一个用户永远落到同一台机器
Nginx ip_hash / cookie优点
- 实现简单
- 代码基本不用改
缺点
- ❌ 扩容困难
- ❌ 节点宕机,连接全断
- ❌ 不是真正分布式
👉 只适合内部系统 / 临时方案
✅ 方案二:Redis / MQ 做“消息中转”(主流方案 ⭐⭐⭐⭐⭐)
核心思想
WebSocket 连接只在本地维护,消息用中间件广播
┌─────────┐
│ Redis │ (Pub/Sub)
└─────────┘
▲ ▲
│ │
┌──────────┘ └──────────┐
│ │
Spring A Spring B
(连接1,2) (连接3,4)流程
- 用户连接到某一台服务
- 服务器:
- 本地保存
userId → WebSocketSession - 同时订阅 Redis Topic
- 本地保存
- 任何服务发消息 → Redis
- 所有节点收到 → 判断是不是自己的用户 → 推送
三、推荐的整体架构(生产可用)
Client
│
▼
Nginx / LB
│
▼
Spring Boot (WebSocket)
├── 本地 Session Map
├── Redis Pub/Sub
└── Redis 存在线用户路由Redis 中存什么?
| Key | 说明 |
|---|---|
| user:{userId} | 该用户在哪台机器 |
| ws:topic | 广播消息 |
⚠️ WebSocketSession 永远只放内存
四、Spring Boot 分布式 WebSocket 实现示例
1️⃣ WebSocket 处理器
@Component
public class WsHandler extends TextWebSocketHandler {
// 本机连接
private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = session.getUri().getQuery(); // demo
SESSIONS.put(userId, session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
SESSIONS.values().remove(session);
}
public static void send(String userId, String msg) throws IOException {
WebSocketSession session = SESSIONS.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(msg));
}
}
}2️⃣ Redis 发布消息
@Service
public class WsMessageService {
@Autowired
private StringRedisTemplate redisTemplate;
public void sendToUser(String userId, String msg) {
redisTemplate.convertAndSend("ws:topic", userId + ":" + msg);
}
}3️⃣ Redis 订阅 & 分发
@Component
public class WsRedisListener {
@Autowired
private StringRedisTemplate redisTemplate;
@PostConstruct
public void subscribe() {
redisTemplate.getConnectionFactory()
.getConnection()
.subscribe((message, pattern) -> {
String body = new String(message.getBody());
String[] arr = body.split(":");
String userId = arr[0];
String msg = arr[1];
try {
WsHandler.send(userId, msg);
} catch (Exception ignored) {}
}, "ws:topic".getBytes());
}
}✔ 每台节点都会收到消息,但只有有 session 的节点能发
五、集群扩展:精准路由(进阶)
如果用户量大,避免广播:
Redis 记录路由
user:1001 -> nodeA
user:1002 -> nodeB发送时:
- 查 Redis 得到 node
- 只向 node 的 topic 发
ws:node:A
ws:node:B👉 这是 IM / 消息系统的标准做法
六、生产级常见坑(非常重要)
❌ 1. 把 WebSocketSession 放 Redis
绝对错误(不可序列化)
❌ 2. 用 HttpSession 共享
WebSocket ≠ HTTP
❌ 3. 忽略心跳
- nginx 默认 60s 会断
- 必须 ping/pong
setInterval(() => ws.send("ping"), 30000)七、什么时候用 MQ 而不是 Redis?
| 场景 | 选择 |
|---|---|
| 即时消息 | Redis Pub/Sub |
| 保证不丢 | RabbitMQ |
| 高吞吐 | Kafka |
八、你可以直接选的结论
👉 90% 项目最佳方案:
Spring Boot + WebSocket + Redis Pub/Sub
#redis #websocket