应迁移到redis stream+consumer group。因pub/sub阻塞、无持久、无ack、难分片,而stream支持多消费者、ack、xpending、自动故障转移及严格递增id,可彻底解决高吞吐下的丢消息、积压与扩展问题。

Redis SUBSCRIBE 阻塞式调用无法应对高吞吐场景
PHP 的 subscribe() 是阻塞式同步调用,一旦进入监听状态,整个进程就卡在那儿,既不能做其他事,也无法横向扩展。当消息频次超过每秒几百条,或单条消息处理耗时波动大(比如涉及 DB 查询、HTTP 调用),就会迅速积压、超时、丢消息——这不是代码写得不好,是模型本身不支持并发消费。
不要在 subscribe 回调里直接处理业务逻辑
常见错误是把订单创建、发邮件、写日志等耗时操作全塞进 subscribe 的回调函数里。这会导致:订阅连接长时间占用、新消息无法及时接收、Redis 服务器端连接数暴涨、PHP 进程被系统 kill(超时或内存溢出)。
- 回调函数里只做最轻量的事:解析
$message、校验格式、投递到本地队列(如pcntl_async_signals(true)+pcntl_signal_dispatch()触发异步任务) - 真正处理交给独立的 worker 进程,例如用
php worker.php启动常驻进程,从 Redis List(LPUSH)或 Beanstalkd 拉取任务 - 避免在回调中调用
sleep()、file_get_contents()、mysqli_query()等阻塞操作
分片订阅需手动控制频道命名与消费者分组
Redis 原生不支持「消费者组」语义(那是 Stream 的能力),Pub/Sub 的「分片」只能靠人工约定。比如按业务类型或 ID 取模划分频道:order:shard:0、order:shard:1……再启动多个 PHP 进程,每个只订阅一个频道。
- 频道名必须固定且可预测,不能动态拼接(否则订阅者无法预知要连哪个)
- 需要外部协调器(如 etcd 或 MySQL 表)记录哪些进程在消费哪个 shard,防止重复或遗漏
- 扩容时要停机重配,无法像
XREADGROUP那样自动 rebalance - 若用
PUBSUB NUMSUB查看订阅数,会发现所有进程都连着同一个 Redis 实例,只是频道不同——这仍存在单点压力
真正可行的解法:迁移到 Stream + Consumer Group
如果你已经卡在 Pub/Sub 的负载瓶颈上,别硬扛。Redis 5.0+ 的 Stream 天然支持多消费者、ACK、pending list 和自动故障转移,比自己拼凑分片+队列靠谱得多。
- 改用
XADD stream:orders * event_type order_created payload ...发布 - 用
XREADGROUP GROUP mygroup consumer-1 COUNT 10 STREAMS stream:orders >拉取消息 - 处理完必须调用
XACK stream:orders mygroup <id></id>,否则消息会留在XPENDING -
consumer-1崩溃后,其他 consumer 可通过XCLAIM接管 pending 消息,无需额外运维逻辑
这个切换不是加几行代码的事,但能避开 Pub/Sub 所有隐性缺陷:无持久、无重试、无顺序保障、无消费者状态跟踪。最容易被忽略的一点是——你写的「分片订阅」脚本,在 Redis 主从切换或网络抖动后,大概率会漏掉中间几条消息,而 Stream 的 last_delivered_id 是严格递增且可恢复的。










