必须用独立*redis.pubsub实例,不可复用redis.client;receive()统一处理message/subscription/pong三类事件,需手动按msg.channel分流;close()必须在goroutine末尾调用,断连需主动重连。

必须用独立 *redis.PubSub 实例,不能复用 redis.Client
混用会 panic 或静默失败。redis.Client 是为 GET/SET 等命令设计的;Subscribe 返回的 *redis.PubSub 底层独占一条 TCP 连接,且禁止执行任何非 Pub/Sub 命令。
错误写法:client.Subscribe(ctx, "order.created") 后立刻调 client.Get(ctx, "x") → 报 redis: nil 或连接重置。
正确做法:新建专用 client(或至少确保生命周期隔离),例如:
subClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
pubsub := subClient.Subscribe(ctx, "user.updated", "order.created")
连接池开销可控,但逻辑耦合的代价是查两小时收不到消息。
Receive() 是首选,别用 ReceiveMessage(ctx) + 短超时轮询
ReceiveMessage(ctx) 只处理业务消息,会忽略 *redis.Subscription 和 *redis.Pong 事件,导致状态不同步;而短超时(如 100 * time.Millisecond)轮询会反复触发 I/O timeout 错误(如 WSARecv tcp ...: i/o timeout),最终耗尽连接稳定性。
正确做法:用无超时的 pubsub.Receive() 阻塞等待下一个有效事件,它能统一处理三类事件:
-
*redis.Message:业务消息 -
*redis.Subscription:订阅/退订确认(需检查msg.Kind == "subscribe") -
*redis.Pong:心跳响应(可忽略,但必须接收)
示例关键片段:
for {
msgI, err := pubsub.Receive()
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, redis.Nil) {
return
}
log.Printf("pubsub receive error: %v", err)
continue
}
switch msg := msgI.(type) {
case *redis.Message:
handleBusinessMsg(msg)
case *redis.Subscription:
log.Printf("sub event: %s %s %d", msg.Kind, msg.Channel, msg.Count)
case *redis.Pong:
// 忽略,但必须消费
}
}
多个频道共用一个 Receive() 流,靠 msg.Channel 手动分流
你可以一次 Subscribe(ctx, "user.updated", "order.created"),但所有消息都走同一个 Receive() 流,不会自动按频道拆 goroutine 或 channel。
必须手动判断:
if msg.Channel == "user.updated" { ... }if msg.Channel == "order.created" { ... }
如果业务逻辑差异大(比如一个要查 DB,一个只发通知),建议拆成多个独立 Subscribe + goroutine,避免慢操作阻塞快通道。
注意:Redis 不保证跨频道消息顺序,只保证单频道内 FIFO;别在多个频道间做强时序依赖。
不 Close() 就泄漏,重连必须自己实现
pubsub.Close() 不是可选项,是释放连接的唯一方式。不调它,Redis 的 CLIENT LIST 里 idle 连接越积越多,最终触发 maxclients 拒绝新连接。
常见错误:在 Subscribe 后直接 defer pubsub.Close() —— 这会让连接立刻关闭,收不到任何消息。
正确姿势:
- 启动 goroutine 处理消息流,
defer pubsub.Close()放在该 goroutine 末尾 - 监听退出信号(如
os.Interrupt),主动调pubsub.Close() - 网络断开后需实现退避重连:捕获
redis.ConnFailureError,sleep 后重建*redis.PubSub
最容易被忽略的是:订阅成功 ≠ 频道名对。Redis 对错名、大小写、空格、连错 DB 都不报错,只安静等待。上线前务必用 context.WithTimeout 主动验证一次 Receive() 是否能拿到消息,否则服务可能静默卡死。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











