gorilla/websocket默认模式撑不住百万连接,因其内存驻留状态、无生命周期钩子、读写缓冲不复用、需手动实现心跳/广播/重连,且连接关闭时无法可靠清理redis状态;5k连接后即抖动,主因是每连接独占8kb缓冲及百万goroutine调度开销,叠加系统ulimit限制未调导致emfile错误。

靠 gorilla/websocket 默认模式撑不住百万连接——不是代码写得差,是它根本没设计成干这个的。内存驻留、无生命周期钩子、读写缓冲不复用、广播/心跳/重连全得手撸,连连接关闭时清理 Redis 状态都不可靠。
为什么 gorilla/websocket 在 5k 连接后就开始抖动
每个连接独占两份 4KB 缓冲区(读+写),100 万连接光缓冲就吃掉 ~8GB;更致命的是 ReadMessage/WriteMessage 是阻塞调用,必须配一个常驻 goroutine 等数据就绪,结果百万 goroutine 堆在调度器上,CPU 花在切换上远多于干活。系统级限制(如 ulimit -n 默认 1024)还没调,accept 就开始返回 EMFILE 错误。
连接注册/注销必须串行化,别锁整个 map
用户狂点刷新,旧连接还没走完 unregister,新连接已进 register,两个 goroutine 同时改 map[*websocket.Conn]bool,轻则状态错乱,重则 panic。用 sync.RWMutex 锁整个连接表?锁粒度太大,高并发下争抢严重。
- 改用带缓冲的 channel 串行处理注册/注销事件:所有操作统一发到
registerCh和unregisterCh - 起一个 dedicated goroutine 消费这两个 channel,顺序执行增删逻辑
- 避免直接操作共享 map,改用原子操作或 per-user key 的 Redis SET(如
user:123:session)做最终一致性兜底
Redis 存什么字段才真正有用
把 *websocket.Conn 塞进 Redis?它不能序列化,跨进程也无效。真正要存的是能重建上下文的元数据,且必须是 JSON 字符串 + 单 key + 过期时间:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
user_id:业务用户标识(不是 conn ID),用于定向推送 -
node_id:当前节点名(如ws-node-02),故障转移时可拉取该节点全部会话 -
room_ids:字符串数组,记录该用户所处房间,广播时免去全量遍历 -
last_heartbeat:毫秒时间戳,服务端主动探测失联比 TCP keepalive 更准 -
conn_created_at:辅助老化淘汰(比如 >24h 无活动自动踢出)
统一存成 JSON,用 SET user:123:session "{...}" EX 3600,别用 HASH——单 key 操作原子性更强,SCAN 批量捞离线用户也更稳。
跨节点广播怎么避免重复和丢失
多个 ws 节点同时收到“用户 A 发消息”,各自遍历本地连接发一遍,订阅者收两次;节点重启又丢消息,客户端断线重连后收不到断连期间的消息。
- 业务层不直连连接,而是
PUBLISH chat:room:123 "{...}" - 每个节点
SUBSCRIBE chat:room:*,收到后只推给本节点内存中room_ids包含123的连接 - 用
LPUSH chat:room:123:buffer缓存最近 10 条,重连时LRANGE补发 - PUB/SUB 不落地,所以 buffer 是必须的兜底,不是可选项
最易被忽略的一点:last_heartbeat 必须由服务端定时更新,不能依赖客户端心跳包——网络抖动时客户端发不出,但服务端仍可基于该字段判断是否失联并主动清理 Redis 状态。这一步漏掉,整个会话生命周期就不可控了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










