直接用 redis.Conn 做 Pub/Sub 容易断连,因其不维护保活和重连逻辑,网络抖动或 Redis 重启会导致静默断开、goroutine 阻塞或 panic;必须单独 goroutine 调用 Receive(),遇 redis.ErrClosed 或 io.EOF 时重建连接并重订阅,且不可复用普通命令连接。

为什么直接用 redis.Conn 做 Pub/Sub 容易断连?
Go 标准库的 redis.Conn(比如 github.com/gomodule/redigo/redis)本身不维护 Pub/Sub 连接的保活和重连逻辑。一旦网络抖动或 Redis 重启,Subscribe 后的连接会静默断开,但你的代码还在等 Receive(),结果就是 goroutine 永久阻塞或 panic。
- 订阅后必须单独起 goroutine 调用
Receive(),不能混在普通命令执行流程里 -
Receive()返回redis.ErrClosed或io.EOF时,必须主动重建连接并重新Subscribe - 不要复用普通读写连接做 Pub/Sub ——
SUBSCRIBE会让连接进入“订阅模式”,之后只能收消息,不能再发GET/SET等命令
用 github.com/go-redis/redis/v8 的 PubSub 类型更稳
它把重连、消息分发、取消机制都封装好了,比手撸红连接更贴近生产需求。关键不是“能不能用”,而是“出问题时你有没有兜底能力”。
- 初始化用
rdb.Subscribe(ctx, "topic1", "topic2"),返回*redis.PubSub - 用
ps.Channel()获取chan *redis.Message,直接 range 消费,底层自动重连 - 务必在退出前调用
ps.Close(),否则 goroutine 泄漏(go-redis内部会起心跳协程) - 如果需要按 pattern 订阅,用
ps.PSubscribe(ctx, "log.*"),收到的消息Pattern字段非空
如何避免多个微服务实例重复消费同一消息?
Redis Pub/Sub 是广播模型,发一次,所有订阅者都收到 —— 这不是 bug,是设计如此。如果你要“一个消息只被一个实例处理”,Pub/Sub 本身不解决这个问题。
- 别试图靠加锁或全局状态来“抢消息”,这违背 Pub/Sub 的语义,也引入单点风险
- 真需要负载均衡式消费,改用 Redis Streams +
XREADGROUP,配合消费者组和 pending list - 或者退一步:让每个微服务只订阅自己负责的 topic(如
user-service:order-created),靠 topic 划分职责 - 若只是日志、监控类广播通知,重复消费完全可接受,反而说明高可用
实际写法示例:启动一个监听 goroutine 并安全退出
别把 pubsub 当成一次性工具,它是个长期存活的组件,生命周期要和 service 对齐。
func startMessageListener(rdb *redis.Client, topics []string) func() {
ps := rdb.Subscribe(context.Background(), topics...)
ch := ps.Channel()
done := make(chan struct{})
go func() {
defer close(done)
for msg := range ch {
switch msg.Channel {
case "order.created":
handleOrderCreated(msg.Payload)
case "user.updated":
handleUserUpdated(msg.Payload)
}
}
}()
return func() {
ps.Close() // 必须调用
<-done // 等 goroutine 结束
}
}
注意:ps.Channel() 返回的 channel 在 ps.Close() 后会关闭,range 自动退出;但没关之前,它不会因为 Redis 断连而关闭 —— 重连期间 channel 保持 open,消息持续流入。
最常被忽略的是:没有给 Subscribe 传带超时的 context,导致初始化卡住时整个服务启不来。上线前至少加个 context.WithTimeout(ctx, 5*time.Second)。


















