因为PHP Redis扩展的subscribe()是同步阻塞调用,底层使用阻塞式socket recv,无法被Swoole协程调度器接管,导致在onRequest或onMessage中调用时整个协程卡死、后续请求全部挂起。

为什么 redis->subscribe() 在 Swoole HTTP/WS 服务里会阻塞?
因为 PHP Redis 扩展的 subscribe() 是同步阻塞调用,它会一直等待 Redis 消息,直到连接断开或超时。而 Swoole 的协程(Swoole\Coroutine\Redis)不支持该命令——底层是阻塞式 socket recv,无法被协程调度器接管。直接在 onRequest 或 onMessage 里调用,整个协程就卡死了,后续请求全被挂起。
解决思路不是“绕过阻塞”,而是把订阅逻辑彻底移出协程上下文,交给独立进程或子协程+非阻塞轮询(但后者不可靠)。最稳的方式是:用 Swoole\Process 启一个常驻子进程,用原生 Redis 扩展做阻塞订阅,收到消息后通过 sendMessage() 或 push() 投递到主协程队列处理。
如何用 Swoole\Process + 原生 Redis 实现不阻塞的订阅进程?
关键点在于:子进程必须用 pcntl_signal_dispatch() 处理信号、用 set_time_limit(0) 关闭超时、手动重连断开的 Redis 连接。不能依赖 Swoole 协程 Redis,它压根没实现 subscribe。
- 子进程中用
new Redis(),调用$redis->connect()+$redis->subscribe(['channel1'], function($redis, $channel, $msg) { ... }) - 在回调里用
$process->write($msg)或Swoole\Coroutine\Channel(需提前在主进程创建并传入)转发消息 - 务必在
subscribe前设置$redis->setOption(Redis::OPT_READ_TIMEOUT, -1),否则默认 5 秒超时会退出循环 - 捕获
RedisException,连接断开时 sleep 1 秒后重试,避免狂刷错误日志
示例片段(主进程启动逻辑):
立即学习“PHP免费学习笔记(深入)”;
$process = new Swoole\Process(function(Swoole\Process $proc) {
$redis = new Redis();
while (true) {
try {
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
$redis->subscribe(['task_queue'], function($redis, $chan, $msg) use ($proc) {
$proc->write("{$chan}:{$msg}\n");
});
} catch (RedisException $e) {
sleep(1);
}
}
});
Swoole\Process::signal(SIGCHLD, function($sig) { Swoole\Process::wait(); });
$process->start();
怎样让 HTTP 请求触发 Redis 发布,又不等消费结果?
发布动作本身很快,但必须确保用的是非阻塞方式。别在 onRequest 里用协程 Redis 的 publish() —— 它虽然不阻塞,但若 Redis 连接池耗尽或网络抖动,仍可能因超时拖慢响应。更稳妥的是:用 Swoole\Coroutine\Redis + 连接池,设置 timeout ≤ 100ms,并忽略失败(日志记录即可)。
- 初始化连接池:
new Swoole\Coroutine\Redis\Pool(['host' => '127.0.0.1', 'port' => 6379, 'timeout' => 0.1]) - 在协程内
$redis = $pool->get()→$redis->publish('task_queue', json_encode($data))→$pool->put($redis) - 不要
awaitpublish 结果;失败时error_log("publish failed: " . $redis->getLastError()) - 如果业务要求强一致性(比如必须确保发布成功),那就得换方案:先写 MySQL 标记待发布状态,再由定时任务或单独消费者补发
消费端怎么保证消息不丢、不重复?
Redis Pub/Sub 本身不保证可靠性:客户端断连期间的消息直接丢失,也没有 ACK 机制。所以它只适合“通知型”场景(如刷新缓存、广播状态变更),不适合订单、支付等关键链路。
真要保可靠,得组合其他机制:
- 发布时同时写一条
LPUSH task_list $msg到 list,消费进程用BRPOP task_list 0阻塞读取(这个可被协程调度) - 消费成功后
LREM task_list 0 $msg或用ack_key记录已处理 ID(需自己维护去重逻辑) - 或者直接弃用 Redis Pub/Sub,改用 Kafka/RabbitMQ,Swoole 有成熟协程客户端支持
很多人卡在“以为 subscribe 就是消息队列”,其实它连基本的持久化都没有。上线前一定确认业务是否真的能容忍丢消息。



















