PHP 性能优化实战:从 3s 到 300ms,我是怎么优化一个接口的?
一次真实的接口优化记录,不谈虚的,只讲实战。
背景:问题从哪来?
这是一个典型的后台列表接口:
- 技术栈:PHP 8.1 + Laravel 10 + MySQL 8
- 场景:管理后台订单列表
- 数据量:订单表 200w+,关联用户表、商品表
- 原始表现:
- 平均响应:3s+
- QPS:不到 10
- 数据库 CPU 经常飙高
接口逻辑很简单:分页查询订单 + 关联用户信息 + 商品信息 + 简单统计。
但"简单"背后,藏着一堆性能坑。
Step 1:先定位,再优化(别瞎猜)
优化第一条原则:先 profiling,再动手。
工具组合
- Xdebug + CacheGrind(本地分析)
- Laravel Debugbar(SQL / 内存)
- MySQL
EXPLAIN(索引问题)
初步结论
Debugbar 一眼看出问题:
- SQL 执行时间:2.5s+
- SQL 条数:N+1 问题严重
- 内存峰值:120MB+
- 重复查询同一个用户 / 商品数据
结论:90% 的问题在数据库。
Step 2:干掉 N+1,这是最大头
原代码(问题版)
$orders = Order::paginate(20);
foreach ($orders as $order) {
$order->user->name;
$order->product->title;
}
Laravel Debugbar 显示:
- 1 条查订单
- 20 条查用户
- 20 条查商品
- 合计 41 条 SQL
优化后(Eager Loading)
$orders = Order::with(['user', 'product'])
->paginate(20);
结果:
- SQL 数量:3 条
- SQL 时间:从 2.5s → 600ms
✅ 一次简单的
with(),性能直接提升 4 倍。
Step 3:索引不是万能,但没索引是万万不能
原查询
select * from orders
where status = 1
and created_at >= '2024-01-01'
order by created_at desc
limit 20 offset 0;
EXPLAIN 一看:
- type:
ALL - rows: 2,000,000
- Using filesort: ✅
加复合索引
ALTER TABLE orders
ADD INDEX idx_status_created_at (status, created_at);
再次 EXPLAIN:
- type:
range - rows: 20
- Using filesort: ❌
效果:
- SQL 时间:600ms → 150ms
✅ 索引是性价比最高的优化手段。
Step 4:分页的 OFFSET 陷阱
当 offset 很大时,性能会急剧下降。
limit 20 offset 100000;
MySQL 依然要扫描前 10 万行。
优化方案:游标分页(Cursor Pagination)
$orders = Order::with(['user', 'product'])
->where('status', 1)
->cursorPaginate(20);
原理:
- 不再使用
offset - 使用
where id > last_id
效果:
- 深度分页响应:从 1s+ → 稳定 100ms 内
✅ 后台列表,99% 场景不需要"跳页",用游标分页刚刚好。
Step 5:减少不必要的数据传输
问题
select *- JSON 返回大量无用字段
- 接口返回 200KB+
优化
Order::select(['id', 'order_no', 'user_id', 'product_id', 'status', 'created_at'])
API Resource 精简字段:
public function toArray($request)
{
return [
'id' => $this->id,
'order_no' => $this->order_no,
'status' => $this->status,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
'product' => [
'id' => $this->product->id,
'title' => $this->product->title,
],
];
}
效果:
- 响应体积:200KB → 30KB
- 网络耗时明显下降
Step 6:缓存,但别乱缓存
适合缓存的
- 商品信息(变化少)
- 统计数据(非实时)
实现方式
$product = Cache::remember(
"product:{$order->product_id}",
600,
fn () => Product::find($order->product_id)
);
注意点
- 缓存 key 要清晰
- 更新数据时清缓存
- 不缓存分页结果(容易脏数据)
效果:
- 接口时间:150ms → 80ms
Step 7:PHP 层面的"小刀割肉"
1. 关闭 debug 模式
APP_DEBUG=false
- 减少日志
- 关闭 Debugbar
- 响应时间减少 20~30ms
2. 开启 OPcache(生产环境必开)
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
效果:
- PHP 执行时间下降 30%+
3. 避免循环内做"重活"
❌ 错误示例:
foreach ($orders as $order) {
User::find($order->user_id);
}
✅ 正确方式:
- 批量查询
- 数组映射
Step 8:最终效果对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均响应 | 3000ms | 300ms |
| SQL 数量 | 40+ | 3 |
| 内存峰值 | 120MB | 35MB |
| QPS | <10 | **80+** |
10 倍性能提升,不是靠"黑科技",而是靠一个个小点堆出来的。
总结:接口优化的"套路"
我总结了一个 PHP 接口优化 checklist,以后照着用:
- ✅ 先 profiling,再动手
- ✅ 干掉 N+1(Eager Loading)
- ✅ 检查索引(EXPLAIN)
- ✅ 避免大 OFFSET 分页
- ✅ 只查需要的字段
- ✅ 合理使用缓存
- ✅ 生产环境开 OPcache
- ✅ 精简 JSON 返回结构
- ✅ 减少循环内的 DB / IO 操作
- ✅ 用 Cursor Pagination 替代传统分页
写在最后
性能优化不是"炫技",而是用最小的成本解决最痛的问题。