Webman无法直面百万QPS,因其协程受限单线程调度、连接池保守、无内置熔断,实测单机超8万QPS即现超时与OOM;应仅作秒杀业务层,流量需由Nginx/OpenResty前置拦截卸载。

秒杀请求压不垮,但 Webman 默认的 HTTP 连接池会先垮
Webman 默认用 Swoole\Http\Server,单进程可扛几千并发,但秒杀场景下真实瓶颈常不在 PHP 层,而在下游——比如 MySQL 连接数打满、Redis 连接超时、甚至 DNS 查询阻塞。你看到的 502 Bad Gateway 或 Connection refused,大概率是 pdo->connect() 或 redis->connect() 在连接池耗尽后直接失败,而非业务逻辑卡住。
实操建议:
- MySQL 必须切到
PDO::ATTR_PERSISTENT => true+swoole_mysql协程客户端(非ext-redis),否则每个请求新建连接,1000 并发 ≈ 1000 个 TCP 连接涌向 DB - Redis 连接必须用
co\Redis或hyperf/redis的协程驱动,禁用phpredis同步模式;配置max_connections至少设为并发预估量的 2 倍 - DNS 查询必须提前
gethostbyname()缓存或改用 IP 直连,避免高并发下getaddrinfo阻塞整个协程调度
Redis 里扣库存,但 DECR 不等于“秒杀成功”
很多人写 $redis->decr('stock:1001') 后判断返回值 ≥ 0 就发券,这在单机 Redis 下勉强可用,但 Webman 多 Worker 部署时,DECR 是原子的,而后续的“生成订单”“发 MQ”不是。一旦 Redis 扣减成功但 PHP 进程崩溃,库存就丢了,用户没下单却少了一件货。
正确做法是把“扣库存 + 写订单”包进 Lua 脚本,或改用 Redis Transaction + WATCH,但更稳妥的是:只用 Redis 做预减,真正落库走 MySQL 悲观锁(SELECT ... FOR UPDATE)校验最终库存,失败则回滚 Redis 并抛 StockNotEnoughException。
注意点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 不要在 Lua 脚本里调用
redis.call('publish'),Swoole 8.0+ 的 Redis 协程客户端不支持脚本内发布消息 - 若用
WATCH,必须确保所有 key 在同一 Redis 分片(否则CROSSSLOT错误),秒杀商品 ID 建议用固定哈希前缀如stock:{floor(id/1000)}:1001 - Redis 预减后加个过期时间(
EXPIRE),比如 10 分钟,防用户下单中途放弃导致库存被锁死
订单写入 MySQL 瞬间变慢?别怪 INSERT,先查 UNIQUE KEY 冲突
秒杀订单表通常有 user_id + sku_id 联合唯一索引防重复下单。但高并发下大量 INSERT ... ON DUPLICATE KEY UPDATE 会触发间隙锁(Gap Lock),尤其当索引不是最左匹配或存在范围查询时,容易造成锁等待雪崩,show engine innodb status 里能看到大量 waiting for table metadata lock。
优化方向很明确:
- 联合唯一索引必须按查询顺序建,比如查
WHERE user_id = ? AND sku_id = ?,索引就得是(user_id, sku_id),反过来会失效 - 用
REPLACE INTO替代INSERT ... ON DUPLICATE KEY,减少锁持有时间(但要注意主键自增 ID 跳变) - 订单表拆成热冷两张:秒杀刚生成的订单进
order_temp(无复杂索引、引擎用Memory或TokuDB),异步消费后才写入主order表
Webman 的 onWorkerStart 不是万能初始化钩子
有人习惯在 onWorkerStart 里初始化 Redis 连接、加载商品缓存、预热路由,觉得“启动一次,全 Worker 共享”。问题在于:Swoole Worker 进程是 fork 出来的,但 Redis 连接句柄(resource)不能跨进程复用,onWorkerStart 里 new 的 co\Redis 实例在其他 Worker 里是 null;更隐蔽的是,PHP 的静态变量在 fork 后各进程独立,你以为的“全局缓存”其实每 Worker 一份,内存翻倍还不同步。
安全做法只有两个:
- 连接类对象(Redis/MySQL)必须在每次请求中 lazy init,或封装成
Container::get(Redis::class)由 DI 容器按协程生命周期管理 - 只读配置类(如商品基础信息)可以用
apcu_store()+apcu_fetch()跨进程共享,但需配合版本号检测更新,不能依赖static $cache - 绝对不要在
onWorkerStart里执行耗时操作(如全量拉 Redis、扫描目录),会导致 Worker 启动延迟,新请求进来时部分 Worker 还没 ready
架构演进不是堆组件,而是不断识别哪个环节在说谎——Redis 说库存还有,MySQL 说已被抢光;Worker 说连接已建好,实际句柄早已失效;缓存说数据最新,其实是 fork 后各自为政。盯住这些“不一致”,比选型更重要。

















