Swoole面试中协程排队与限流考查的是onRequest毫秒级入口内并发控制、状态同步、维度隔离三者协同能力;Channel因协程级隔离且多Worker不共享而无法用于跨请求排队,须用Swoole\Table+Atomic实现单机限流,Redis全局限流必须用EVAL脚本保障原子性并设熔断降级。

协程排队和限流在 Swoole 面试里,从来不是考你能不能手写一个队列类,而是看你能不能在 onRequest 这个毫秒级入口里,把并发控制、状态同步、维度隔离三件事同时做对——漏掉任意一环,线上就可能被压垮。
为什么用 Swoole\Coroutine\Channel 做排队会失效
很多人第一反应是“用 Channel 控制并发数”,但 Channel 是协程级的,每个请求进来都 new 一个新 Channel,根本起不到排队作用;更危险的是,在多 Worker 进程下,每个进程都有自己的 Channel 实例,完全不共享,限流阈值直接乘以 Worker 数。
- Channel 适合协程间协作(比如生产者-消费者模型),不适合跨请求排队
- 真正要排队,得用进程间共享结构:
Swoole\Table存等待队列长度 +Swoole\Atomic控制入队原子性 - 若必须用 Channel,只能放在全局变量里(如
$server->taskworker中预创建),但需配合超时清理,否则 Channel 积压导致内存泄漏
Swoole\Table 实现单机限流的三个硬约束
Table 是单机限流最常用方案,但它不是“开了就能用”,有三个运行时硬约束必须手动满足:
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- Table 必须在
WorkerStart回调中创建,不能在onRequest里动态 new —— 否则每次请求都新建,内存爆炸 - key 设计必须包含限流维度,例如
"limit:ip:".$request->header['x-real-ip'],否则所有 IP 共用一个计数器 - incr 操作后必须立刻判断返回值是否超阈值,不能先 incr 再 get —— 因为 incr 本身是原子的,get 会多一次查表开销且破坏原子语义
Redis 全局限流为什么不能只用 INCR + EXPIRE
两步操作在高并发下必然出现竞态:INCR 成功但 EXPIRE 失败,导致 key 永久存在;或者多个请求同时 INCR 后都去 EXPIRE,浪费性能。
- 必须用
EVAL脚本封装原子逻辑,例如:redis.call("INCR", KEYS[1]) if tonumber(redis.call("GET", KEYS[1])) == 1 then redis.call("EXPIRE", KEYS[1], ARGV[1]) end return redis.call("GET", KEYS[1]) - 脚本中
KEYS[1]必须带完整限流维度(如"rate:uid:1001:/api/order"),否则不同用户或接口会互相覆盖 - 别忽略 Redis RT:单次 EVAL 在万级 QPS 下可能拖慢整个
onRequest,建议加熔断(如连续 5 次 >50ms 就降级走本地 Table)
协程排队时最容易被忽略的 timeout 处理
排队不是等就行,协程挂起后若没超时机制,一个卡死的下游依赖会让整个队列阻塞,后续请求全在 Channel 或 Table 里堆积。
- 用
Channel::pop(1.0)而不是Channel::pop(),显式设 1 秒超时 - 超时后必须主动释放资源:比如已占位的连接池连接要
close(),已预占的 Table 计数要decr() - 别忘了记录日志:
error_log("queue timeout for ip ".$ip." path ".$path),否则问题发生时连入口都定位不到

















