直接用 redis.conn.subscribe 会卡住,是因为它仅返回 *redis.pubsub 实例,必须显式启动接收循环(如调用 receive() 或监听 channel()),否则消息永不进入;且需为 pub/sub 单独配置连接池、禁用闲置关闭,并用缓冲 channel 解耦接收与业务处理,避免阻塞导致丢消息。

为什么直接用 redis.Conn.Subscribe 会卡住?
Go 官方 github.com/go-redis/redis/v8 不提供阻塞式 Subscribe,但很多人误以为调用 client.Subscribe(ctx, "channel") 后就能立刻收消息——实际它只返回一个 *redis.PubSub 实例,**必须显式启动接收循环**,否则消息永远不进来。
常见错误是写完 sub := client.Subscribe(ctx, "notify") 就去干别的事,结果日志里完全没输出。
- 正确做法:立即调用
sub.ReceiveMessage(ctx)或启动 goroutine 跑sub.Channel() -
ReceiveMessage是同步阻塞的,适合单 channel 简单监听;Channel()返回chan *redis.Message,适合多 channel 复用或与 select 配合 - 别忘了用
defer sub.Close(),否则连接泄漏,Redis 侧会堆积 SUBSCRIBE 连接
如何避免 redis: connection pool timeout 导致订阅中断?
Pub/Sub 连接和普通命令连接不同:Redis 要求客户端保持长连接,而默认的 redis.Options.PoolSize(通常是 10)会被普通命令抢占,导致 Subscribe 拿不到连接,报超时。
根本原因不是 Redis 崩了,而是连接池被占满 + Pub/Sub 连接未复用。
- 为 Pub/Sub 单独建 client:用
redis.NewClient(&redis.Options{Addr: "...", PoolSize: 1}),固定只用 1 连接,且永不归还给共享池 - 禁用闲置关闭:
IdleCheckFrequency: -1,防止心跳机制误杀长连 - 不要在同一个 client 上混用
Get/Set和Subscribe,命令连接池和 pub/sub 连接语义冲突
收到消息后怎么安全通知业务逻辑?
直接在 sub.Channel() 的 for-range 循环里调用数据库写入或 HTTP 请求,会导致消息处理阻塞后续接收——Redis Pub/Sub 是「发即忘」,但 Go 的 channel 接收端一旦卡住,Redis 就会开始丢消息(尤其高并发时)。
这不是 Redis 问题,是消费者吞吐跟不上。
- 用带缓冲的 channel 中转:
msgCh := make(chan *redis.Message, 100),接收 goroutine 只负责转发,不处理 - 另起 worker goroutine 池消费
msgCh,比如for i := 0; i - 消息体建议 JSON 解析前先校验
message.Payload非空,避免 panic;错误消息可打日志+推到 dead-letter channel,别 panic 后整个 goroutine 退出
如何让多个 Go 实例共享同一份通知而不重复触发?
Redis Pub/Sub 是广播模型:所有订阅者都会收到完整副本。如果你有 5 个 API 实例都 Subscribe("order_paid"),那一笔订单支付就会触发 5 次下游通知。
这不是 bug,是设计使然。要实现「集群内仅一次执行」,得换思路。
- 改用 Redis Streams(
XADD/XREADGROUP):天然支持 consumer group,每条消息只被组内一个实例消费 - 如果必须用 Pub/Sub,加一层协调:收到消息后先
SETNX notify:order:123 "done" EX 30,成功才处理,失败则跳过——依赖 Redis 原子性 - 注意:SETNX 方案在极端重试场景下可能漏通知(比如进程 crash 在 SETNX 后、处理前),生产环境优先选 Streams
真正麻烦的从来不是订阅代码那几行,而是连接生命周期管理、消息可靠性边界、以及多实例语义对齐——这些地方不细想,上线后出问题很难定位。











