go-redis 订阅短频道一直等待,检查 RESP3 的36字节预读
go-redis v9.21.0固定Peek36会在短且完整的RESP3订阅确认上等待。v9.23.0正式修正按已缓冲数据解析,升级需Go1.26,并验证真实空闲连接。
go-redis 升到 v9.21.0 后,Subscribe() 已经发出订阅,紧接着的 PubSub.Receive() 却不返回。频道名换长一点又恢复。如果连接使用 RESP3,且订阅确认之后没有别的流量,要检查客户端是否在等待一个并不存在的第 36 字节。
这条问题针对短的订阅确认帧,不代表 Redis 没收到 SUBSCRIBE。重试业务订阅之前,先区分“服务器没有回复”与“回复已经到达、客户端还没开始解析”。
完整确认只有35字节,Peek却要求36字节
单次订阅 aaaaaa,订阅数量为 1 时,RESP3 确认可以是:
>3\r\n$9\r\nsubscribe\r\n$6\r\naaaaaa\r\n:1\r\n
这里用可见的 \r\n 表示协议里的回车换行。实际帧只有 35 字节。v9.21.0 的 PeekPushNotificationName() 先调用 bufio.Reader.Peek(36),而 Peek 要等够指定长度才返回;确认本身已经完整,空闲连接却不会再补一个字节,于是解析器没有机会处理它。
在这个单频道、数量为 1 的帧形下,六个 ASCII 字节的频道名会卡在边界,七个字节可以达到 36。不要把这推成“所有六字符名字都坏”:中文的 UTF-8 字节数、确认类型、订阅计数及同一连接上已经缓冲的其他数据,都可能改变帧长。
升级解析器,同时核对 Go 工具链
正式 v9.23.0 已包含修正:先解析已经缓冲的内容,只在前缀有效而尚不完整时等一个新增字节。它不会为了识别通知名先要求固定 36 字节。
选用包含修正的正式版本前,检查模块的 Go 版本要求。v9.23.0 要求 Go 1.26,不能只改 go.mod 中的依赖版本,却继续用更旧的构建工具链。保留现有认证、TLS、地址、协议选择及连接池配置,确认运行产物实际包含了目标客户端。
暂时无法升级时,只有业务不依赖 RESP3 特性、且兼容性验证允许,才可评估在现有客户端选项上显式采用 Protocol: 2。这会改变协议行为,应测试原有命令与订阅消费;不能为消除等待就随意给线上频道改名,生产者和消费者可能因此订阅不同的地址。
有界等待不能代替修复
诊断已有订阅时,可使用 PubSub 的有界接收接口,避免观察线程一直挂住。以下片段放在已有函数中,ctx 和 sub 来自应用原来的订阅生命周期:
reply, err := sub.ReceiveTimeout(ctx, 3*time.Second)
片段需要导入 time;调用方仍须处理 err 和 reply 的实际类型。超时只说明这次接收没有在期限内完成,不证明 SUBSCRIBE 未生效,也不能通过不断启动新 goroutine 来“取消”原来的阻塞读取。诊断结束后按既有流程关闭专用 PubSub 对象。
验收应使用独立、空闲的订阅连接,覆盖短频道名、较长名字、连续多次确认和真实消息接收;不要依靠测试期间恰好有其他流量把等待冲开。还要检查断连重订阅和 goroutine 回收。Redis Pub/Sub 本身不保存断开期间的消息,重建连接也不能补回历史消息;需要可靠投递的业务应延续自己的确认或持久化设计。


