WebSocket网关不可直连Redis Pub/Sub广播,因连接爆炸、跨实例消息隔离、无重连回溯、频繁启停触发maxclients限制;须用全局PubSub实例+连接池复用,按规范命名频道,严格序列化与消息过滤。

为什么不用 redis-cli 直连做 WebSocket 广播
直接用客户端连 Redis 发布消息,再让每个 WebSocket 连接自己订阅频道——这看似简单,实际会迅速崩掉。每个连接维持一个 redis-py 的 PubSub 实例,内存和 socket 句柄消耗极大;更麻烦的是,当网关横向扩容时,不同实例间的订阅完全隔离,A 实例发的消息,B 实例的用户收不到。
- 单机部署下看似能跑,但一加机器就丢消息
-
PubSub连接不支持自动重连 + 消息回溯,断连期间消息直接消失 - WebSocket 连接生命周期短,频繁启停
PubSub实例容易触发 Redis 的maxclients限制
用 redis-py 的 ConnectionPool + 全局 PubSub 实例
核心是复用连接、解耦生命周期:整个网关进程只起一个 PubSub 实例监听频道,收到消息后通过内存队列或事件总线(如 asyncio.Queue)转发给对应的 WebSocket 连接。发布端则统一走连接池,不依赖订阅端状态。
- 初始化时创建单例
ConnectionPool,传给Redis和PubSub两个对象 -
PubSub必须调用pubsub.subscribe()后立即启动pubsub.run_in_thread(sleep_time=0.001),否则阻塞主线程 - 不要在
message_handler里直接 await WebSocket send —— 需投递到 event loop,比如用asyncio.create_task()
ps = redis_client.pubsub()
ps.subscribe('ws:room:123')
ps.run_in_thread(sleep_time=0.001) # 后台线程持续监听频道命名必须带业务前缀,且避免通配符订阅
用 ps.psubscribe('*') 看似省事,但 Redis 通配符订阅性能差、无法精准控制权限,而且一旦有其他系统往 Redis 写杂乱频道,你的网关会收到一堆无关消息,CPU 白白拉高。
- 推荐格式:
ws:notify:{user_id}、ws:room:{room_id}、ws:service:order_update - 动态频道(如用户私信)务必在连接建立时订阅,断开时
unsubscribe,防止内存泄漏 - 别用中文或空格作频道名,Redis 对字节敏感,
ws:订单更新和ws:%E8%AE%A2%E5%8D%95%E6%9B%B4%E6%96%B0是两个频道
发布端要处理序列化与空值,订阅端得校验消息结构
Redis 的 PUBLISH 只收 bytes 或 str,如果后端服务直接 publish('ws:room:123', {'msg': 'hi'}),Python 会报 TypeError: a bytes-like object is required;而前端收到 raw 字符串又没法解析。
- 发布前统一
json.dumps(data).encode('utf-8'),别漏ensure_ascii=False - 订阅端收到后先
try/except json.loads(msg['data']),失败就跳过,别让一条脏数据卡住整个PubSub线程 - 别依赖
msg['type'] == 'message'就认为是业务消息——Redis 会发subscribe/unsubscribe类型的控制消息,必须过滤
WebSocket 网关里 Redis Pub/Sub 不是“接上线就能用”的功能,真正的难点在生命周期对齐:连接何时订阅、何时退订、消息如何不丢不重、扩容后怎么保证广播一致性。这些没对齐,哪怕代码跑起来,压测一上就露馅。










