redis pub/sub的tps受限于单核cpu,因publish全程在主线程同步执行:遍历订阅者、序列化消息、写入各客户端缓冲区,无法并行;pubsub numsub/channels等o(n)命令也会阻塞主线程;redis 6.0的i/o多线程对此无效。

Redis 发布订阅(Pub/Sub)的 TPS 直接受限于单核 CPU 性能,根本原因在于 PUBLISH 命令的执行路径完全跑在主线程里 —— 它不是“只是网络收发”,而是要遍历所有匹配的订阅者、序列化消息、逐个写入客户端输出缓冲区,整个过程无法并行。
为什么 PUBLISH 是纯主线程操作?
Pub/Sub 没有持久化、不涉及磁盘 I/O,也不走后台线程队列(比如 BIO_LAZY_FREE),它的全部逻辑都在事件循环中同步完成:
-
PUBLISH触发后,主线程立即查找所有匹配该 channel 或 pattern 的 client 列表 - 对每个匹配 client,把消息写入其
client->buf或client->reply链表(内存拷贝) - 后续网络写回仍由同一主线程在下一轮事件循环中通过
writeToClient发出 - 没有异步拆分、没有批量跳过、没有多线程分发 —— 即使你有 1000 个订阅者,也得串行处理完这 1000 次写入
PUBSUB NUMSUB 和 PUBSUB CHANNELS 也会拖慢 TPS
这两个命令看似只读,但它们在内部会遍历整个订阅哈希表(server.pubsub_channels)或 pattern 树(server.pubsub_patterns),时间复杂度是 O(N)。当 channel 数量大或 pattern 匹配规则多时,一次调用就可能阻塞主线程数毫秒,直接拉低整体吞吐。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
PUBSUB CHANNELS *在 channel 数超 10k 时可能卡住主线程 >5ms -
PUBSUB NUMSUB ch1 ch2 ch3 ...参数越多,遍历开销越线性增长 - 这些命令和
PUBLISH共享同一线程,任何阻塞都会让后续所有请求排队
Redis 6.0+ 的 I/O 多线程对 Pub/Sub 完全无效
Redis 6.0 引入的多线程仅作用于网络读(io-threads-do-reads yes)和写(io-threads),但 Pub/Sub 的瓶颈不在 socket 接收/发送层,而在命令执行本身:
- I/O 多线程只加速
read(2)和write(2)系统调用,不加速内存遍历、消息序列化、client 缓冲区填充 -
PUBLISH执行前,消息已完整解析;执行后,响应早已在内存里 —— I/O 线程根本没机会插手 - 实测:开启 4 个 I/O 线程后,1000 订阅者的
PUBLISHTPS 几乎无提升(误差
真正能缓解 Pub/Sub TPS 瓶颈的手段
别指望靠调参或升级版本绕过单线程限制,必须从架构和用法上做取舍:
- 用
SSUBSCRIBE(Redis 7.0+)替代传统 pattern 订阅:它基于跳表索引,O(log N) 查找,比pubsub_patterns的链表遍历快一个数量级 - 避免 wildcard pattern(如
news.*),改用显式 channel 分组(news.sports,news.tech),减少遍历范围 - 把高扇出(fan-out > 500)场景迁移到专用消息队列(如 Kafka / NATS),Redis Pub/Sub 不适合做广播中枢
- 如果必须用 Redis,横向扩实例 + 客户端做 channel 分片(如 hash(channel) % N),让不同 channel 落在不同 CPU 核心上
最常被忽略的一点:Pub/Sub 的“实时性”是假象 —— 主线程一旦被慢命令(如 KEYS、大 HGETALL)卡住,所有订阅消息都会积压在内存里,直到主线程腾出手来逐个 flush。这不是网络延迟,是单线程调度不可绕过的硬约束。










