分享一次高并发下单场景下 PHP 性能优化的完整实战
问题背景
上个月我们接手了一个电商项目,平时流量不算大,但每逢整点秒杀就会迎来一波瞬时峰值。上线前做了压测,结果很扎心:每秒 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 开发者来说,不要因为一门语言的性能天花板就认命。合理做异步化、用对中间件,业务代码同样能扛住大规模并发。这次踩了不少坑,写出来也算是一种复盘吧。
欢迎评论区聊聊你们的优化经验,如果有不同的防超卖方案也欢迎分享出来,大家一起交流。
年卡会员