必须用redis做状态同步,因gorilla/websocket等库无跨进程状态共享能力;http/grpc实时查询存在延迟、竞态、性能瓶颈;redis只存轻量路由元数据(如user:uid:node、node:id:conns),用原子命令操作,消息通过publish/subscribe转发并去重,键过期时间须≥心跳间隔×2。

必须用 Redis 做状态同步,不是“推荐”,而是架构层面的刚性约束——gorilla/websocket 和 net/http + websocket.Upgrader 本身不提供跨进程连接状态共享能力,每个节点只持有自己内存里的 *websocket.Conn,无法感知其他节点上的连接存活情况。
为什么不能靠 HTTP/gRPC 接口实时查连接状态
看似简单直接的方案(比如 A 节点调 /api/user/status?uid=123 问 B 节点“用户在线吗?”)在生产环境会高频出错:
- HTTP 请求有网络延迟和失败率,一次返回 “未连接” 可能只是 B 节点正卡在 GC 或日志刷盘,
*websocket.Conn其实还活着 - 并发下存在竞态:A 查到“已连接”后准备发消息,B 的连接却刚好因网络闪断关闭,消息直接丢失
- 每条业务消息都触发一次跨节点 HTTP 查询,QPS 上不去,还会把节点间带宽打满、拖垮整个集群
Redis 里该存什么、不该存什么
关键原则:只存轻量路由元数据,绝不存连接对象本身。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- ❌ 不要序列化
*websocket.Conn存 Redis —— 它不可序列化,且强绑定 goroutine 生命周期和底层net.Conn,反序列化后毫无意义 - ✅ 应存三类结构:
user:123:node→"node-02"(用户当前归属节点);node:node-02:conns→ Set 类型,存 UID 列表(如123,456);room:general:members→ Set,存房间成员 UID - 所有写操作必须用原子命令:
SET user:123:node node-02 EX 300 NX防覆盖;DEL user:123:node+SREM node:node-02:conns 123必须成对出现在conn.Close()的defer或context.Done()回调中
消息转发时如何避免丢、重、循环
用户 A 在 node-01 发消息给 B,B 实际连在 node-02,转发链路设计错误会导致雪崩:
- ❌ 不要让 node-01 直接连 node-02 的 gRPC 接口推送 —— node-02 若正在重启或高负载,请求会堆积超时,阻塞整个发送队列
- ✅ 正确做法:node-01 将消息
PUBLISH到 Redis channelchannel:msg;所有节点SUBSCRIBE该 channel;收到后先查本地node:node-xx:conns确认目标 UID 是否真在本节点,再投递 - 防重复:消息体带全局唯一
msg_id(建议用time.Now().UnixNano() + 节点ID拼接),节点收到后执行SETNX seen:msg_id 1 EX 60,60 秒内同 ID 消息只处理一次
EXPIRE 时间设多少才安全
这是最容易被忽略、但最致命的一环:Redis 键的过期时间必须 ≥ 心跳间隔 × 2。
例如你 WebSocket 心跳设为 30 秒(客户端每 30 秒发一次 ping),那么 user:123:node 的 EX 至少得设成 90 秒。否则,若某次心跳延迟(比如 GC STW 导致 pong 响应慢了 40 秒),Redis 就可能在连接还活着时误删路由信息,导致后续消息全部发错节点或丢失。










