不能复用 redis.client 做 pub/sub,因为 *redis.pubsub 独占 tcp 连接且协议不兼容普通命令,混用会导致 panic 或静默失败(如 redis: nil、connection reset);必须新建独立 client 实例并显式 close。

为什么不能复用 redis.Client 做 Pub/Sub
直接用同一个 *redis.Client 实例调 Subscribe() 后再执行 Get() 或 Set(),会 panic 或静默失败——因为 *redis.PubSub 独占 TCP 连接,且协议层不兼容普通命令。错误现象包括:redis: nil、read: connection reset by peer、或收不到任何消息但日志全无报错。
必须新建独立 client 实例,例如:
subClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 5, // 不要设太大,Pub/Sub 连接不走连接池
})
pubsub := subClient.Subscribe(ctx, "user.created", "order.paid")
defer pubsub.Close() // 注意:defer 在循环里无效,必须显式 close
- 别省代码复用主 client;连接开销远小于调试两小时找不到收不到消息的原因
- 每个
*redis.PubSub实例只服务一个订阅生命周期,不要跨 goroutine 复用 - 若需多频道,用单个
Subscribe()调用传多个 topic,而非多个实例
如何确认 Subscribe 真的生效了
Subscribe() 返回非 nil 不代表已连上频道——Redis 对错名、大小写、空格、连错 DB 都不报错,只会安静等待。真正确认信号是收到 redis.Subscription 类型消息,它带 Kind == "subscribe" 且 Count >= 1。
实操必须做三件事:
- 启动后立即监听
pubsub.Channel()流,过滤出msg.Kind == "subscribe"的消息 - 检查
msg.Channel是否匹配你传入的 topic(注意大小写和前缀一致性) - 加超时验证:用
context.WithTimeout(ctx, 5*time.Second)包裹首次ReceiveMessage(),超时则手动用redis-cli PUBSUB NUMSUB your_topic查活跃订阅数
漏掉这步,上线后“卡住”却查不出原因,是最常见的静默故障。
ReceiveMessage() 怎么避免卡死或漏事件
ReceiveMessage() 是阻塞调用,但它可能返回三类不同语义的 error:正常取消、心跳超时、真实连接断开。统一 if err != nil { break } 就等于放弃整个 goroutine。
-
errors.Is(err, context.Canceled):主动退出,应pubsub.Close()并 return -
errors.Is(err, redis.Nil):心跳超时,忽略,继续循环 - 其他任意 error(如
read: connection closed):连接已断,需记录日志 + 重建*redis.PubSub实例
别在循环里加 time.Sleep——所有等待必须通过 ctx 控制超时;也别用 select { case 做兜底,那会掩盖真实连接状态。
Publish 返回值为 0 是不是消息丢了
rdb.Publish(ctx, "topic", data).Val() 返回的是**当前活跃订阅客户端数量**,不是发送成功与否。它为 0 只说明此刻没人在线,不代表消息丢失——Pub/Sub 本身就不保证投递,消息发出去就没了。
- 若业务需要可靠投递,别用 Pub/Sub,改用 Redis Streams 或外部 MQ
- 若坚持用 Pub/Sub,发布端需配合心跳检测 + 订阅端上报存活状态,否则无法感知“无人订阅”
- 高频发布时注意 Redis 的输出缓冲区,默认 10 条,满后新消息直接丢,且不报错;可通过
client-output-buffer-limit pubsub调整,但治标不治本
最易被忽略的点:Pub/Sub 没有重试、没有 ACK、没有消费位点,它只是一个广播喇叭——设计之初就该接受“尽力而为”的事实。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











