每个 subscribe 连接必须独占一个 tcp 连接,因订阅模式下仅允许 publish、unsubscribe 等有限命令,与业务读写互斥;应使用专用连接池和长期存活的 pubsub 实例,避免短连接风暴与消费阻塞,并优先选用 redis stream 保障消息可靠性。

每个 SUBSCRIBE 连接必须独占一个 TCP 连接
Redis 的 SUBSCRIBE 命令会将连接切换到「订阅模式」,此后该连接只能处理 PUBLISH、UNSUBSCRIBE、PING、QUIT 等有限命令。一旦执行了 GET 或 SET,就会报错:ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed。这意味着你不能复用一个普通 Redis 连接来同时做业务读写和订阅——它俩是互斥状态。
常见错误是:在 Web 请求里临时创建 redis-py 的 PubSub 实例,处理完就丢弃。这会导致:
- 每请求新建一个连接,快速触达
maxclients限制 - 大量
TIME_WAIT状态堆积,尤其在高并发短连接场景下 -
Connection reset by peer错误频发,本质是连接被服务端主动关闭后客户端还试图发命令
用专用连接池 + 长期运行的 PubSub 实例
核心原则是:一个连接池专供订阅,一个 PubSub 实例长期存活,多个 channel 复用它。不要按需启停。
以 redis-py 为例:
- 禁用全局共享连接池(如
redis.Redis(connection_pool=shared_pool)),改用独立池:order_pubsub_pool、inventory_pubsub_pool - 初始化时创建一次
PubSub,调用p.subscribe('order:created', 'order:paid')订阅多个 channel - 用
p.parse_response(block=True, timeout=0.1)轮询,配合queue.Queue把消息投递给工作线程,避免listen()阻塞主线程 - 进程生命周期内保持该实例运行,不随请求销毁
Java 用户用 Lettuce 时同理:为每个业务域配独立的 StatefulRedisPubSubConnection 和 ClientResources,避免 event loop 线程共享导致状态混乱(比如出现 READONLY You can't write against a read only replica)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
别碰 net.core.rmem_max 这类内核参数
很多人一遇到卡顿就去调大 socket buffer,但对 Pub/Sub 毫无意义。Redis 发完消息就丢,不等 ACK;如果客户端收不过来,recv buffer 积压只是表象,根源是消费逻辑太慢或连接太少。
真正该调的只有三处(仅当连接风暴明显时):
-
net.core.somaxconn:设为65535,防 TCP 握手队列溢出 -
fs.file-max:系统级文件句柄上限,建议 ≥200000 -
net.ipv4.tcp_fin_timeout:从60降到30,加快TIME_WAIT回收(只对短连有效)
其他如 rmem_max、wmem_max、tcp_tw_reuse 对 Pub/Sub 性能无实质改善,反而可能掩盖真实瓶颈。
异步驱动 + 批处理 + 超时熔断才是高吞吐解法
阻塞式 listen() 在消息洪峰下必然积压。必须把「接收」和「处理」拆开:
- Python 中用
parse_response(timeout=0.1)非阻塞轮询,控制单次等待时长 - 收到一批消息后,批量提交到线程池或 asyncio task,避免逐条同步处理
- 加超时熔断:若某 channel 消息处理耗时 >5s,记录告警并跳过后续几条,防雪崩
- 别依赖自动重连——
get_message()返回None或抛ConnectionError时,应重建PubSub实例并重订阅,而不是死等
最易被忽略的一点:Pub/Sub 本身不保证可靠性。消息丢了不会重发,消费者挂了就彻底错过。如果业务需要不丢消息,得直接迁移到 Stream,别硬撑。










