必须用Redis做状态同步,因gorilla/websocket等不支持跨进程连接状态共享;Redis应存user:uid:node等轻量路由元数据而非Conn对象,写操作须用原子命令,EXPIRE时间需为心跳间隔2倍以上以防误删。
必须用 redis 做状态同步,其他方案在生产环境基本不可靠。 不是“可以选 redis”,而是 go 的 gorilla/websocket 或原生 net/http + websocket.upgrader 本身不提供跨进程连接状态共享能力——每个节点只知道自己手上的 *websocket.conn,没法知道用户连在哪台机器上。
为什么不能靠 gRPC 或 HTTP 接口查连接状态
有人尝试让 A 节点调用 B 节点的 /api/user/conn-status?uid=123 来确认连接是否存在。这看似可行,但实际会踩三个坑:
- HTTP 请求有延迟和失败风险,一次查询可能返回“未连接”,其实只是 B 节点刚好卡在 GC 或写日志,
*websocket.Conn还活着 - 并发场景下,A 节点查到“已连接”后准备发消息,B 节点的连接却刚好断开(如网络闪断),消息就丢了
- 每条消息都走一次跨节点 HTTP 查询,QPS 上不去,还放大了节点间网络压力
Redis 存什么?不是存 Conn,是存路由元数据
别把 *websocket.Conn 序列化塞进 Redis——它不可序列化,且绑定 goroutine 和 net.Conn 生命周期。真正该存的是轻量级路由信息:
-
user:123:node→"node-02"(用户当前连接所在的节点 ID) -
node:node-02:conns→Set类型,存所有活跃 UID(如123,456) -
room:general:members→Set,存房间成员 UID,用于广播前快速取交集
所有写操作必须用 Redis 的原子命令,比如用 SET user:123:node node-02 EX 300 NX 防止覆盖;删除用 DEL + SREM 组合,且要在 conn.Close() 的 defer 或 context.Done() 回调里触发。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
消息转发时怎么避免循环或重复
当用户 A 在 node-01 发消息给用户 B,而 B 实际连在 node-02,node-01 查 Redis 知道该转发,但转发路径设计不对就会出问题:
- 不要让 node-01 直接连 node-02 的 gRPC 接口发消息——如果 node-02 正在重启,请求会超时堆积
- 正确做法:node-01 把消息推到 Redis 的
channel:msg(用PUBLISH),所有节点都SUBSCRIBE该 channel;收到后先校验target_uid是否在本节点的node:xxx:conns里,再投递 - 为防重复,消息体里带
msg_id(用time.Now().UnixNano()+ 节点 ID 生成),节点用SETNX seen:msg_id 1 EX 60去重,60 秒内同 ID 消息只处理一次
最易被忽略的一点:Redis 的 EXPIRE 时间必须比 WebSocket 心跳间隔长至少 2 倍。比如你设心跳 30s,那 user:xxx:node 的过期时间至少得设成 90s——否则网络抖动导致一次心跳没收到,Redis 就删了路由记录,用户瞬间“失联”。

















