先说说一个常见的误区:很多开发者用 Redis 的 PSUBSCRIBE 实现事件路由,结果线上 CPU 飙升、延迟抖动频繁,却始终找不到原因。其实问题的根源很清楚——每次执行 PUBLISH 时,都要线性遍历所有 pattern 进行 glob 匹配,没有索引、没有缓存、也没有短路机制。pattern 数量越多,速度越慢;大小写敏感且 * 无法匹配空字符串,容易导致误配;一旦 pattern 超过 20 个或发布频率达到 100 qps 以上,CPU 毛刺几乎必定出现。

PSUBSCRIBE 每次 PUBLISH 都要遍历全部 pattern 做 glob 匹配
Redis 服务端并未为 pattern 建立索引或前缀树。PSUBSCRIBE 注册的每个 pattern 都老老实实地存放在一个链表中。每次执行 PUBLISH channel message 时,Redis 必须遍历所有已注册的 pattern,对当前 channel 字符串逐个调用 glob 匹配函数(类似 shell 的 *、?、[abc] 规则)。虽然不是正则引擎,但仍然是纯 CPU 密集型操作。
有过这种经历的人一定不陌生:PUBSUB NUMPAT 返回值窜到 200 以上,单次 PUBLISH 延迟从 0.1ms 飙升到 1.8ms——这多出来的开销,几乎全部消耗在 glob 循环和字符串比对上了。
- pattern 越多,遍历成本线性上升:100 个 pattern ≈ 100 次独立匹配
- pattern 越长(比如
service.order.v2.us-east-1.*.failed),单次匹配 CPU 耗时越长 - 没有短路优化:即使第一个 pattern 就匹配成功,Redis 仍然检查完所有 pattern
glob 匹配无大小写控制且不支持空字符通配
Redis 的 glob 实现严格区分大小写,而且 * 不会匹配空字符串——这两个限制经常被误用。结果就是:本该命中的 pattern 实际没触发,但 CPU 照样白跑一遍。
来看几个典型场景:如果你写了 PSUBSCRIBE order.*,它可以匹配 order.pay、order.123,但 不匹配 order(末尾没有点);写了 PSUBSCRIBE order.*.*,它能匹配 order.user.created,但 PSUBSCRIBE order.*.?* 这种带冗余符号的写法,不仅无效,还让匹配逻辑更慢。
order.*→ 不匹配order(*不匹配空字符串)Order.*≠order.*(默认大小写敏感)order.*.?→ 要求 channel 至少有两个点,否则失败;?必须对应一个真实字符,不能“跳过”
客户端无法预判 pattern 是否生效,只能靠发布后观察
Redis 没有提供类似 TESTPATTERN channel pattern 的调试命令。你在订阅前没法验证 PSUBSCRIBE user.* 能不能覆盖 user:123——因为冒号 : 不等于点 .,所以根本不会命中。结果就是:pattern 挂上了,CPU 照算,消息却发不出去。
性能影响在高频发布场景下尤其明显:假设每秒 PUBLISH 1000 次,平均每个有 5 个 pattern 匹配成功,但背后是每秒 1000 × N 次 glob 调用(N = 总 pattern 数)。若 N=80,就是每秒 8 万次字符串扫描。
- 没有缓存机制:相同 channel 反复发布,每次仍重做全部 pattern 匹配
- 无法按 channel 前缀分流:所有 pattern 一视同仁,无分组、无优先级
- 运维排查困难:收不到消息时,得手动比对 channel 名、pattern 写法、大小写、连接状态三层才能定位
替代方案比硬扛 PSUBSCRIBE 更可控
如果业务真需要多维路由(比如按 tenant + type + status 组合过滤),别堆 pattern。用 Redis Streams 配合消费者组,或者在应用层做二级分发(比如用 HSET 存订阅关系,PUBLISH 后由 worker 查表投递)。
真正适合 PSUBSCRIBE 的,只有低频、粗粒度、可预测的场景,比如 config.*、alert.critical 这类固定前缀的运维通知。一旦 pattern 数量超过 20 个,或发布频率超过 100 qps,就得警惕 CPU 毛刺和延迟抖动。
最容易被忽略的一点:pattern 匹配开销发生在 PUBLISH 路径上,而不是订阅端——也就是说,哪怕只有一个客户端 PSUBSCRIBE,只要它挂了几十个 pattern,就能拖慢整个实例的发布吞吐。
