本文以“可验证”为核心,提供最小可复现用例与日志度量方法,指导在 Laravel 11 中消除 N+1 并构建稳定的缓存策略。
## 版本与环境校验
php -v
php artisan --version # 期望输出 Laravel Framework 11.x
composer show illuminate/database | findstr /I versions
## 可复现的 N+1 现象与度量
假设关系:`User hasMany Post`。
// App\Models\User.php
public function posts() { return $this->hasMany(Post::class); }
// 观测 N+1(错误示例)
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
$users = \App\Models\User::limit(50)->get();
foreach ($users as $u) {
$count = $u->posts->count(); // 每个用户一次查询 → N+1
}
dump(count(DB::getQueryLog())); // 1(用户查询)+N(posts 查询)
消除 N+1:
DB::flushQueryLog();
DB::enableQueryLog();
$users = \App\Models\User::with('posts')->limit(50)->get();
foreach ($users as $u) {
$count = $u->posts->count(); // 使用已预加载的数据
}
dump(count(DB::getQueryLog())); // 2:users + posts(in ...)
验证点:查询日志数量应由 `1+N` 降到 `2`;在接口压测下 `p95/p99` 延迟显著下降。
## with、load 与 loadMissing 的适用场景
- `with()`:查询阶段预加载;适用于列表页或明确需要的关系。
- `load()`:查询后再加载;适用于在已有结果上补充关系。
- `loadMissing()`:仅加载“未加载”的关系;避免重复查询。
$users = \App\Models\User::with(['posts' => fn($q) => $q->latest()->limit(5)])
->whereActive(true)
->paginate(20);
$users->loadMissing('profile'); // 若未加载则补齐
## 选择性字段与计数聚合
减少无用字段可进一步降低 I/O:
$users = \App\Models\User::select(['id','name'])
->withCount('posts') // 生成 posts_count 字段
->with(['posts:id,user_id,title']) // 仅取必要字段
->paginate(20);
## 缓存策略(Redis 驱动可验证)
缓存列表与详情的建议:
use Illuminate\Support\Facades\Cache;
// 列表缓存(带页码)
$key = sprintf('users:list:p=%d', $page);
$data = Cache::remember($key, 60, function () use ($page) {
return \App\Models\User::select(['id','name'])
->withCount('posts')
->paginate(20, ['*'], 'page', $page)
->toArray();
});
// 详情缓存(带关系)
$user = Cache::remember('user:'.$id, 300, function () use ($id) {
return \App\Models\User::with(['posts' => fn($q) => $q->latest()->limit(5)])
->findOrFail($id);
});
说明:
- Redis 驱动支持标签缓存(`Cache::tags([...])`),便于按业务维度批量失效;使用前确认驱动为支持标签的 Store。
- 对列表页建议缓存序列化后的数组(`toArray()`),避免模型序列化带来的兼容性差异。
## 实战对比与压测流程
接口压测(示例):
wrk -t 4 -c 200 -d 60s http://127.0.0.1:8000/users --latency
对比维度:
- 关闭缓存 + 未预加载(基线)
- 预加载 + 无缓存
- 预加载 + 缓存(命中率 ≥ 80%)
记录 `RPS/p95/p99/错误率` 与数据库查询数(`DB::listen`/查询日志)。目标是在目标并发下维持低延迟且数据库连接稳定。
## 事务与并发注意事项
- 避免在事务中触发大量懒加载;在进入事务前显式 `with()`。
- 写操作完成后及时失效相关缓存键(或使用标签进行批量失效)。
- 防止缓存穿透:对不存在的资源缓存短期空值(如 30s)。
## 框架级优化配套
php artisan config:cache
php artisan route:cache
php artisan view:cache
## 结语
通过预加载(`with/load/loadMissing`)与有纪律的缓存策略,N+1 可被系统化消除。以查询日志和压测数据为依据进行迭代调参,才能在生产场景稳定获得延迟与吞吐的双重收益。

发表评论 取消回复