Redis PUB/SUB在单线程下支撑十万级并发,关键在于PUBLISH仅做内存拷贝与缓冲区追加,不执行网络发送;真正瓶颈是客户端输出缓冲区的堆积与清空效率,而非命令执行本身。

Redis 的 PUB/SUB 在单线程模型下能支撑十万级并发分发,关键不在于“它多快”,而在于它根本没在主线程里做真正的“分发”——所有订阅者的消息投递,都是同步写入客户端输出缓冲区,不涉及遍历、序列化、网络发送等耗时环节。
为什么 PUBLISH 命令本身几乎不耗时
当你执行 PUBLISH channel message,Redis 主线程只做三件事:
- 查出所有订阅了
channel的客户端(通过内部的哈希表pubsub_channels或模式匹配表pubsub_patterns) - 对每个匹配的客户端,把消息内容(含协议头)直接追加到其
client->buf或client->reply链表中 - 标记该客户端为“有数据待写出”,由后续 I/O 多路复用循环统一处理
整个过程全是内存拷贝 + 指针操作,没有系统调用、没有锁竞争、不触发网络 write()。哪怕 10 万个客户端都订阅了同一个 channel,只要它们的输出缓冲区还有空间,PUBLISH 就能在微秒级完成。
client->buf 和 client->reply 缓冲区才是吞吐瓶颈所在
真正制约 PUB/SUB 并发分发能力的,不是命令执行,而是客户端缓冲区的堆积与清空效率:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
client->buf是固定大小的字节数组(默认 16KB),写满后自动切换到client->reply(sds 链表) - 如果某个订阅者消费慢(比如网络卡顿、客户端不及时读),它的缓冲区会持续增长,最终触发
client-output-buffer-limit pubsub限制(默认 32MB/60s) - 一旦超限,Redis 会强制关闭该连接 —— 这是保护主线程不被拖垮的核心机制
- 缓冲区清理依赖于 I/O 多路复用事件循环:只有当 socket 可写时,主线程才调用
writeToClient()尝试发送,失败则继续挂起
为什么不能靠多线程加速 PUB/SUB 分发
即使你强行把消息分发逻辑拆到后台线程,也会立刻撞上三个硬伤:
- 主线程必须保证
PUBLISH的原子性:要么全部客户端收到,要么一个都不发(实际做不到,但语义上要求“发即可见”)。多线程写不同 client 缓冲区会破坏这个隐含契约 - 多个线程同时修改同一 client 的
reply链表,需要加锁或 CAS,反而引入竞争和开销,得不偿失 - 网络 write() 本身仍需主线程参与(Redis 6.0+ 的 IO 线程只负责 read 和解析,write 仍在主线程);分发再快,卡在 socket 写不出去也没用
真实压测中容易被忽略的点
十万级 PUB/SUB 并发分发在实验室能跑通,但在生产环境常因以下原因断崖式下跌:
- 大量小消息导致频繁内存分配(
sdsnewlen调用增多),触发主线程 CPU 占用上升 - 客户端未启用 TCP_NODELAY,导致 Nagle 算法把多个小响应包合并,增大端到端延迟
- 使用 pattern subscribe(
PSUBSCRIBE)且 pattern 数量多,每次PUBLISH都要遍历全量 pattern 表,复杂度从 O(N_subscribers) 变成 O(N_subscribers × N_patterns) - 监控只看
redis-cli --stat的 ops/sec,却没观察client_longest_output_list和connected_clients是否持续上涨
真正决定 PUB/SUB 承载力的,从来不是“单线程能不能干”,而是“你有没有让主线程一直忙于无效等待”。缓冲区管理、网络参数、订阅拓扑设计,比线程数重要得多。

















