分享一次高并发下单场景下 PHP 性能优化的完整实战

阿乐
阿乐 管理员 年卡会员
发布于 2026-09-03 14:01 ·0 浏览 ·0 回复

问题背景

上个月我们接手了一个电商项目,平时流量不算大,但每逢整点秒杀就会迎来一波瞬时峰值。上线前做了压测,结果很扎心:每秒 2000 并发下单请求,PHP 进程直接被打满,数据库连接池被耗尽,接口响应时间从 80ms 飙升到 12 秒,系统彻底不可用。

排查之后发现,瓶颈不只是代码写得差,还有架构层面的问题。这篇文章就是把这次完整的优化过程记录下来,希望能给大家一些可复用的思路。

第一次压测:惨不忍睹

压测工具用的 JMeter,配置 500 个线程并发下单。结果如下:

- QPS:320(远低于预期的 2000)
- 平均响应时间:2600ms
- 错误率:37%
- CPU 使用率:90%(其中 PHP-FPM 占了 80%)

为什么这么慢?先看代码,问题立刻暴露出来了。

逐个击破:从代码里找致命点

问题一:每次下单都查了三次库存

原来的逻辑大概是这样的:

public function createOrder($userId, $skuId, $num)
{
    // 1. 查商品信息
    $product = Product::find($skuId);
    // 2. 查库存
    $stock = Stock::where('sku_id', $skuId)->first();
    // 3. 再次验证库存(防超卖)
    $stock = Stock::where('sku_id', $skuId)->where('stock', '>', 0)->first();
    // 4. 校验通过后扣减
    $stock->decrement('stock', $num);
    // 5. 创建订单...
}

一次请求查了三次数据库,五步操作串行执行,加锁也没有。这不是在高并发下会挂,是必然挂。

问题二:用数据库行锁兜底超卖,代价太大

框架里用的是 Laravel,不少人习惯用 `lockForUpdate()` 来做悲观锁。这种方法在小并发下没问题,但在高并发场景下,事务持有锁期间所有请求都在等待,数据库行锁竞争激烈,导致大量超时。

问题三:下单后执行了太多同步业务

原来的逻辑里用户下单后要同步执行:发送短信通知、调用第三方支付接口预下单、推送消息到队列(这里队列是假的,其实是同步调用)。这些操作加起来的耗时比核心下单逻辑还长。

优化方案落地:三步走

第一步:Redis 预扣库存,扛住瞬时流量

把库存从数据库搬到 Redis,利用原子操作避免超卖:

// 初始化库存时
$redis->setex('stock:sku:' . $skuId, 86400, 100);

// 下单时
$stock = $redis->decr('stock:sku:' . $skuId);
if ($stock < 0) {
    // 库存不足,回补
    $redis->incr('stock:sku:' . $skuId);
    return '已售罄';
}
// 下单成功后异步写库

用 `decr` 的原子性来扣减,确保在并发情况下不会超卖。Redis 单线程模型天然避免了竞态问题。同时引入 Lua 脚本把后续的流程做原子性控制,更稳妥。

第二步:下单流程异步化,核心链路和次要链路解耦

把订单生成分成两类操作:

- 同步操作:校验 SKU 是否有效(用 Redis 做缓存)、Redis 扣库存、创建订单主记录、返回下单成功
- 异步操作:库存流水记录(MySQL)、发送短信、更新销量统计、推送履约中心

用 RabbitMQ 处理异步任务,PHP-FPM 不再背着短信、推送这些沉重的包袱,响应时间立刻降下来了。

第三步:MySQL 层的防重和最终一致性

内存中的库存最终要和数据库一致。方案是记录异步任务,让 worker 消费队列,用 `UPDATE ... WHERE stock > 0` 做一次最终的数据库扣减,这样既不阻塞主流程,又保证最终一致。

另外还有一个很隐蔽的问题——订单号生成。

原来的代码是用 `time() . mt_rand(1000, 9999)` 去生成,高并发下重复率极高,导致每次插入订单表时发生主键冲突,白白浪费了 DB 资源。改成 Redis INCR 生成序号码,再配合日期前缀,轻松解决。

最终的压测结果

优化完成后还用了同样配置重新压测:

- QPS:从 320 提升到 5600
- 平均响应时间:从 2600ms 降到 180ms
- 错误率:从 37% 降到 0.5%
- CPU 使用率:从 90% 稳定到 45%

实际上瓶颈已经转移到了网关层,应用侧的 CPU 保持在一个健康水位。

最后聊聊这次实践的几点感触

高并发优化核心思路是:别让 PHP 干太多活,能交给 Redis 的不要给 MySQL,能异步的绝不同步。

很多团队一上来就改 SQL、加索引,但真正吸收并发冲击的重要环节在 Redis 层和队列层。代码写得再精细,如果架构上所有请求都穿透到数据库,怎么优化都是杯水车薪。

另外,对 PHP 开发者来说,不要因为一门语言的性能天花板就认命。合理做异步化、用对中间件,业务代码同样能扛住大规模并发。这次踩了不少坑,写出来也算是一种复盘吧。

欢迎评论区聊聊你们的优化经验,如果有不同的防超卖方案也欢迎分享出来,大家一起交流。

全部回复 0

还没有回复,来抢沙发~