go-redis/v9的PubSub不自动重连是设计使然,网络抖动或Redis重启后实例会永久阻塞或panic;必须监听Err()、Close()后重建订阅,用缓冲chan解耦接收与业务处理,并加锁保护handler注册。

go-redis/v9 的 PubSub 实例不重连是设计使然,不是 bug
直接用 client.Subscribe() 返回的 *redis.PubSub 长期监听,网络抖动或 Redis 重启后,它会永久卡在 ReceiveMessage() 上阻塞,或返回 redis.Nil / net.OpError 后 panic。这不是客户端缺陷,而是 v9 明确将连接生命周期与 PubSub 实例绑定——它只负责“当前连接内”的消息流转。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须监听
pubsub.Channel().Err(),一旦收到非nil错误(如redis.Nil、io.EOF、context.Canceled),立刻pubsub.Close()并重建整个订阅流程 - 不要复用旧的
*redis.PubSub,每次重连都调用新client.Subscribe(ctx, ch) - 重连间隔需退避,例如从 100ms 开始,指数增长至 5s,避免雪崩式重连打满 Redis
消息接收循环里别直接处理业务逻辑
for range pubsub.Channel() 是阻塞式消费,若在循环体内解析 JSON、调用数据库、发 HTTP 请求,会导致后续消息积压甚至丢弃——因为 PubSub 内部只有一个接收 goroutine,它卡住,新消息就进不来。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 把
msg.Payload推入一个带缓冲的chan string(如make(chan string, 1024)) - 另起 goroutine 消费该 chan,做 JSON 解析、校验、handler 调用等耗时操作
- 缓冲区大小要结合消息峰值预估;过小易丢消息,过大占内存;建议初始设为 256~2048
注册 Handler 时并发写 map 必须加锁
如果用全局 map[string]func(context.Context, []byte) error 存 handler,又没加锁,热更新或多个服务实例同时注册同名 handler,就会触发 fatal error: concurrent map writes。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.RWMutex包裹注册和查找逻辑:读多(每条消息都要查 handler)用RLock(),写少(仅部署/配置变更时)用Lock() - 注册前检查 key 是否已存在,存在则直接
return fmt.Errorf("handler %q already registered", name),不覆盖 - handler 签名统一为
func(context.Context, []byte) error,把反序列化交给业务层,调度器只管转发原始 payload
JSON 解析失败导致 panic 的常见场景和防御方式
生产环境里 json.Unmarshal(msg.Payload, &task) 很容易 panic:字段缺失、类型错位(比如字符串 "30s" 往 time.Duration 里塞)、空字符串赋给非指针 string 字段等。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 任务结构体所有字段声明为指针类型(
*string、*int64、*time.Duration),并加json:",omitempty"标签 - 绝不省略
if err != nil判断;失败时记录完整msg.Payload和err.Error(),然后continue - 对必填字段(如
JobID、HandlerName)做二次校验:if task.JobID == nil || *task.JobID == ""
真正难的不是写通一条消息路径,而是在断连恢复、消息乱序、字段演进、handler 热更这些边界条件下,依然保证每条消息可追踪、不静默失败、不 panic 崩溃。重连逻辑、消息缓冲、handler 注册锁、JSON 容错这四点,漏掉任一环,上线后都会变成深夜告警。

















