redis pubsub 容易丢消息,因其非线程安全、默认缓冲区仅10条、无超时控制易卡死goroutine、重连不自动恢复订阅、无ack机制且panic或慢处理会导致缓冲区溢出。

为什么直接用 redis.PubSub 容易丢消息
Go 官方 Redis 客户端(github.com/go-redis/redis/v9)的 PubSub 类型不是线程安全的,且内部缓冲区极小(默认 10 条),一旦消费不及时,新消息会直接被丢弃,连错误提示都没有。
-
PubSub.Receive()是阻塞调用,但若在for range循环里没做超时控制,整个 goroutine 可能卡死在某条慢消息上,后续消息全积压到缓冲区溢出 - 客户端断连重连后,
PubSub实例不会自动恢复订阅,必须手动ps.Subscribe(),而重连期间发布的消息完全不可见 - 没有内置重试或 ack 机制,遇到 panic 或未捕获 error 时,当前消息就丢了,且无法回溯
封装时必须自己接管连接生命周期
不能把 redis.UniversalClient 和 redis.PubSub 当作全局单例传入封装层 —— 前者可复用,后者是短命对象,每次重连都得重建。
- 用
redis.NewPubSub创建实例前,先检查底层连接是否健康:client.Ping(ctx).Err(),失败则重建PubSub - 每个
PubSub实例只绑定一个context.Context,并在 context cancel 时显式调用ps.Close(),否则 goroutine 泄漏 - 不要复用同一个
PubSub实例去Subscribe多个频道;不同频道建议分实例,避免一个频道异常影响其他
消息处理必须加 recover + 超时控制
用户注册的回调函数可能 panic、阻塞或耗时过长,这会拖垮整个 PubSub.Receive() 循环,导致缓冲区迅速填满。
- 每个消息处理都包一层
defer func() { recover() }(),防止 panic 传播 - 用
context.WithTimeout给回调加硬性超时,比如ctx, _ := context.WithTimeout(parentCtx, 5*time.Second) - 如果回调返回 error,建议记录日志但不中断循环;若需重试,应把消息内容发回另一个 retry channel,而非原地重执行(Redis Pub/Sub 本身不支持 nack)
如何判断订阅是否真正生效
调用 ps.Subscribe("chan1") 后,Redis 并不会立刻返回确认,而是通过后续收到的 redis.Message 或 redis.Subscription 类型消息来反馈状态。很多人误以为调用完就 OK 了,结果一直收不到消息。
立即学习“go语言免费学习笔记(深入)”;
- 必须监听
ps.Channel()中的redis.Subscription消息,检查Channel字段和Count(>=1 表示已成功加入) - 首次订阅后,至少等 1–2 条
Subscription消息(subscribe/unsubscribe)后再开始处理业务消息,否则可能漏掉第一条 - 生产环境建议加心跳检测:定期
PINGRedis 并检查ps.Ping()是否返回redis.Nil(表示连接正常)










