Redis服务端不会自动清理崩溃残留的订阅,因不保证连接断开时原子清理pubsub_channels中的客户端节点,导致幽灵订阅:PUBSUB NUMSUB返回非零值但无人接收消息,新实例复用频道名时与旧残留形成竞争消费。

必须显式调用 UNSUBSCRIBE,否则残留订阅会持续接收消息但无人处理,造成静默丢消息或干扰新实例。
为什么崩溃后频道不会自动清理
Redis 服务端不保证连接断开时原子清理 pubsub_channels 中的客户端节点。若消费者在 SUBSCRIBE 后瞬间崩溃(如 SIGKILL),服务端可能还没完成注册;高负载下被动清理也可能延迟数秒甚至更久。此时会出现“幽灵订阅”:
-
PUBLISH channel msg仍成功返回,但消息被直接丢弃(无客户端接收) - 新实例上线复用相同频道名,与旧残留订阅者形成竞争消费
-
PUBSUB NUMSUB channel返回非零值,监控误判消费者存活
Go 客户端(redis/v9)崩溃恢复的关键动作
不能依赖 ps.Close() 或进程退出——它只关连接,不退订。正确流程需主动发 UNSUBSCRIBE 并等待确认:
- 用
context.WithCancel()创建可取消上下文,传给rdb.Subscribe(ctx, "channel") - 启动 goroutine 调用
ps.ReceiveContext(ctx),循环中检查errors.Is(err, context.Canceled) - 收到
SIGTERM时,先调ps.Unsubscribe(ctx)(v9.0+ 支持带 ctx 的退订),再调cancel() - 避免在
ps.Close()后还调ReceiveContext()——此时会返回redis.Nil或 panic
Python redis-py v4+ 的安全退订写法
pubsub.close() 只关闭连接,不退订。必须显式调用 unsubscribe():
- 单频道:使用
pubsub.unsubscribe("channel"),它内部执行UNSUBSCRIBE channel并等待服务端确认 - 多频道:传入列表,如
pubsub.unsubscribe(["ch1", "ch2"]) - 全退订:调用
pubsub.unsubscribe()(无参数),退订当前连接所有频道 - 禁止手写
r.execute_command("UNSUBSCRIBE", "channel")——它不等待响应,无法确认是否生效 - 生产环境建议加超时:
pubsub.unsubscribe("channel", timeout=3)(需客户端支持)
如何验证 UNSUBSCRIBE 是否真正生效
调用 unsubscribe() 后不能立刻退出,必须等待服务端回传 type="unsubscribe" 消息:
- 启用
ignore_subscribe_messages=True,过滤掉subscribe/unsubscribe自身通知 - 轮询
pubsub.get_message(),检查msg["type"] == "unsubscribe"且msg["channel"] == "target" -
msg["data"]在成功退订时恒为整数0,可作为额外校验项 - 务必加超时保护(如 5 秒),避免无限等待——网络异常时
get_message()可能阻塞
幽灵订阅不是边缘情况,而是高频故障点。最容易被忽略的是:在容器滚动更新、K8s Pod 重建、或 SIGTERM 处理逻辑里漏掉 UNSUBSCRIBE 调用,或者调了但没等确认就提前退出。只要没看到 type="unsubscribe" 的响应,就不能认为退订完成。











