Redis订阅必须循环调用ReceiveMessage持续读取消息,否则消息堆积;应使用Subscribe(ctx, "a", "b")一次性订阅多频道,避免goroutine泄漏和连接浪费。

redis.Client.Subscribe 之后必须调用 ReceiveMessage,否则连接不收消息
很多人写完 client.Subscribe(ctx, "topic") 就以为监听开始了,其实只是建了个订阅连接,没主动读,Redis 的消息就堆在 TCP 缓冲区里,既不报错也不触发回调。
-
ReceiveMessage是阻塞式读取,每次调用只拿一条;循环调用才是持续监听的正确姿势 - 别用
pubsub.Channel()直接 range —— 它返回的是 unbuffered channel,一旦没人消费,后续Publish会卡住(尤其在测试时表现诡异) - 务必在
for循环里检查err,遇到redis.Nil或上下文取消要主动退出,否则 goroutine 泄漏
多个频道订阅要用 Subscribe(...string) 而非多次 Subscribe
想同时监听 "user.created" 和 "order.paid"?别写两遍 client.Subscribe(ctx, "user.created") 再 client.Subscribe(ctx, "order.paid") —— 这会建两个独立连接,浪费资源,且无法统一管理生命周期。
- 正确做法是传入可变参数:
client.Subscribe(ctx, "user.created", "order.paid") - 这样只建立一个
*redis.PubSub实例,ReceiveMessage返回的msg.Channel字段能告诉你这条消息来自哪个频道 - 如果频道名是运行时拼的(比如带用户 ID),记得提前校验空值和非法字符,Redis 不会拒绝,但可能被中间件拦截或导致路由混乱
context.WithValue 传身份标识,别传业务数据
有人喜欢在 context 里塞 userID、reqID 甚至整个结构体,方便日志追踪。这没问题,但千万别把需要序列化的业务数据(比如订单详情)也塞进去——context 不是传输载体。
-
context.Context只该存轻量元信息,如"publisher_name"、"trace_id",用于日志打标或超时控制 - 消息体必须走
Publish的message any参数,并显式 JSON 序列化,否则类型丢失、中文乱码、大对象 panic 都可能发生 - 如果你发现
msg.Payload是空字符串或乱码,先检查发布端是否用了json.Marshal,再确认订阅端是否用json.Unmarshal解析,而不是直接类型断言
goroutine 泄漏比消息丢更快地拖垮服务
最隐蔽也最致命的问题:忘记 defer pubsub.Close(),或在 Subscribe 后没配 ctx 超时,导致连接永远挂着。
立即学习“go语言免费学习笔记(深入)”;
- 每个
Subscribe都启一个后台 goroutine 拉 Redis 流,不关就一直占内存+网络 fd - 用
context.WithTimeout(ctx, 30*time.Second)包一层再传给Subscribe和ReceiveMessage,强制兜底 - 上线前跑一遍
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=1,看有没有几百个卡在redis.(*PubSub).ReceiveMessage的 goroutine










