发布于2026-07-09 阅读(0)
扫一扫,手机访问
聊到 Golang 消息推送,很多人的第一反应就是开协程、堆并发。但真正在生产环境踩过坑的人都知道,并发只是入场券,真正决定系统能不能撑住 30 万活跃 PV 的,是三道闸门:连接怎么管、消息怎么分、失败怎么兜。用错一道,QPS 就卡在半山腰;用对了,4c8g 的机器也能扛得住。

直接说结论:连接层、分发层、存储层,每一层都有自己的门道。下面逐个拆解。
net.Conn 泄漏或卡死WebSocket 连接不是建完就完事的。如果不加管控,net.Conn 在高并发下会迅速吃光文件描述符,或者让 TCP TIME_WAIT 暴涨到让内核告警。真实场景里,90% 的连接异常都出在心跳和读写超时的设置上——要么没设,要么设错了。
readLoop 和 writeLoop goroutine,千万别共用一个循环来读写。为什么?因为 conn.Write() 可能阻塞,一旦堵住,心跳 ping 就发不出去了。time.AfterFunc 定时发送 websocket.PingMessage,客户端回复 pong 后重置定时器。如果超时未响应,比如连续 90s 没有动静,直接 conn.Close(),别犹豫。userID、deviceID、tags 等)存入 sync.Map,不要查 DB 或 Redis。热路径上的一次远程调用,就是延迟放大器。sync.Pool 复用,避免高频分配加剧 GC 压力。但注意:池中的对象用完后要显式清空字段,比如把 userID 置为 0,否则可能残留脏数据。把所有在线用户塞进一个 map[userID]*Conn 然后 for-range 推送,这是典型的性能反模式。360 万用户里哪怕只有 10% 在线,单次广播就得遍历 36 万个连接,CPU 直接打满。
"order_update")哈希到 64 个 shard,再在 shard 内按 userID % 16 路由到子队列。每个子队列配一个独立的 chan *Message 和固定数量的 worker(比如 4 个),保证并发可控。Publish。改用内存缓冲:同一 channel 的消息攒够 50 条,或者 100ms 触发一次批量 redis.Client.Publish。这样一来,QPS 能降 90%,延迟反而更稳定。map[string]map[uintptr]struct{} 来存——key 是 topic,value 是 conn 指针集合。增删操作使用 sync.RWMutex,但锁内只做指针操作,绝不调用 DB 或 RPC。tag=ios_vip,用布隆过滤器做预筛选,再加后置校验,避免每次推送都全量匹配字符串。"已发送"不等于"已送达"。线上最常见的坑是:没有做 ack 去重,结果用户收到 3 条一模一样的订单通知;或者重试逻辑写在了主线程里,一条慢请求拖垮了整个分发链路。
messageID(用 uuid.NewV7() 或时间戳+序号),客户端收到后主动上报 ACK {messageID, userID}。服务端用 redis.SetNX("ack:"+messageID, "1", time.Hour) 保证幂等。timer.AfterFunc 实现),如果仍然失败,则写入 Kafka 或 RocketMQ 的 retry_topic,由独立的消费者兜底处理。context.WithTimeout(ctx, 3*time.Second),超时直接丢弃并触发告警,绝不能阻塞主流程。zset 存储未读消息 ID,score 设为过期时间戳,再定时用 ZRANGEBYSCORE 清理过期数据。说到底,消息推送真正难的从来不是"怎么推",而是"推错了之后怎么收场"。重试队列堆积时要不要自动降级?用户连续断连 3 次后该不该暂停推送?这些边界逻辑如果没写进代码,压测时永远看不出问题,一旦上了生产,就会给你"惊喜"。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8