Go用go-redis做Redis Pub/Sub必须处理阻塞、断连、并发、消息积压四类问题:Subscribe后需带超时验证频道有效性,ReceiveMessage()会因错频道名无限挂起;禁止在接收循环中做耗时操作,须用缓冲channel解耦;多频道需手动分流;断连后必须手动重连并重新Subscribe,且采用指数退避策略。

Go 用 go-redis 做 Redis Pub/Sub,不是“连上就能收”,必须处理阻塞、断连、并发、消息积压四类问题;否则上线后静默丢消息、连接数暴涨、goroutine 卡死,排查起来像在黑盒里摸电路。
Subscribe 后 Receive(ctx) 必须带超时验证频道有效性
订阅成功不等于频道存在或有人发消息。Redis 对错频道名、空格、大小写、跨 DB 都不报错,pubsub.ReceiveMessage(ctx) 会无限挂起,CPU 低、日志空、服务像冻住。
- 上线前强制加 5 秒超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),调一次pubsub.ReceiveMessage(ctx) - 若返回
context.DeadlineExceeded或errors.Is(err, redis.Nil),立刻用redis-cli PUBSUB NUMSUB your_channel查是否为 0 - 别只写
if err != nil { break }——redis.Nil是心跳超时,不是错误,要显式忽略
ReceiveMessage 循环体内禁止做耗时操作
pubsub.Channel() 底层只有一个接收 goroutine,它卡住,新消息就进不来。JSON 解析、DB 查询、HTTP 调用全堆在 for range 里,等于把整个订阅通道堵死。
- 把
msg.Payload推入缓冲 channel:ch := make(chan string, 1024)(初始建议 256~2048) - 另起 goroutine 消费:
go func() { for payload := range ch { /* 处理逻辑 */ } }() - 缓冲区过小 → 消息被丢;过大 → 内存浪费;峰值流量高时需压测调优
多个频道共用一个 PubSub 实例,必须手动分流
你可以 client.Subscribe(ctx, "user.created", "order.paid"),但所有消息都走同一个 pubsub.Channel(),不会自动按频道分 goroutine 或 channel。
立即学习“go语言免费学习笔记(深入)”;
- 必须显式判断:
if msg.Channel == "user.created" { handleUserCreated(msg.Payload) } - 如果业务差异大(一个查 DB、一个只打日志),建议拆成两个独立
Subscribe+ 独立接收循环,避免慢通道拖垮快通道 - Redis 不保证跨频道消息顺序,只保证单频道 FIFO;别在
user.created和user.updated之间强依赖时序
断连后不重建 = 永久失联,重连必须退避且不复用旧实例
go-redis/v9 的 *redis.PubSub 不自动重连是设计使然——它绑定当前 TCP 连接,网络抖动或 Redis 重启后,ReceiveMessage() 要么阻塞、要么 panic,不会自己恢复。
- 必须监听
pubsub.Channel().Err(),收到非 nil 错误(io.EOF、redis.Nil、net.OpError)立即pubsub.Close() - 每次重连都调新
client.Subscribe(ctx, ch),绝不要复用旧*redis.PubSub - 重连间隔用退避策略:从 100ms 开始,每次 ×2,上限 5s,防雪崩
最常被忽略的点:Redis 服务器不保存客户端订阅状态,连接断开即丢失所有 SUBSCRIBE;所谓“自动重连”客户端库,只重建 TCP,从不帮你重新 SUBSCRIBE —— 这一步必须手写,且要带超时验证。


















