## 复现实验场景
- 模型:`User` 一对多 `Post`
- 数据量:用户 10 万、每用户帖子 10~50
// 伪代码示例(Seeder)
User::factory(100000)->create()->each(function ($u) {
Post::factory(random_int(10,50))->create(['user_id' => $u->id]);
});
## N+1 问题与优化
// 问题代码:N+1(1 次取用户 + N 次取 posts)
$users = User::limit(1000)->get();
foreach ($users as $u) {
$posts = $u->posts; // 触发额外查询
}
// 优化:预加载(eager loading)
$users = User::with('posts')->limit(1000)->get();
foreach ($users as $u) {
$posts = $u->posts; // 不再额外查询
}
// 计数优化:避免加载大集合
$users = User::withCount('posts')->limit(1000)->get();
// 按需补充:避免重复查询
$users->loadMissing('posts');
// 严格模式:阻止懒加载(Laravel 9+)
Model::preventLazyLoading();
## 指标采集与验证
- 使用 `Laravel Telescope` 或 `barryvdh/laravel-debugbar` 统计查询次数与耗时;
- 或 `DB::enableQueryLog()` + `DB::getQueryLog()` 手工采集;
- 期望:从 `1 + N` 次查询降为 `2~3` 次(`users` 与关联 `posts`)。
## 缓存策略
// 典型列表缓存(避免重复构建)
$key = 'users.index.page1';
$users = Cache::remember($key, now()->addMinutes(10), function () {
return User::with(['posts' => fn($q) => $q->select('id','user_id','title')])
->orderByDesc('id')
->limit(1000)
->get();
});
// 细粒度缓存:统计类数据
$count = Cache::remember("user:{$userId}:posts_count", now()->addMinutes(30), function () use ($userId) {
return Post::where('user_id', $userId)->count();
});
## 注意事项与边界
- 预加载字段尽量裁剪:避免传输/序列化过量;
- 结合分页或游标:大表场景控制内存与响应时间;
- 缓存失效策略要与写路径一致:更新/删除需主动 `forget`;
- 高并发下使用 `remember` 的互斥(cache stampede)防护,或引入二级缓存;
- 生产环境开启 `Model::preventLazyLoading()` 有助于在开发阶段及时暴露问题。
## 结论
- 预加载是解决 N+1 的首选;计数/聚合优先用 `withCount/withSum`;
- 将热点列表与统计数据纳入缓存,显著降低数据库压力;
- 指标需以实际查询次数、响应时间与资源占用为准进行持续验证。

发表评论 取消回复