本文以“可验证”为核心,提供最小可复现用例与日志度量方法,指导在 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 可被系统化消除。以查询日志和压测数据为依据进行迭代调参,才能在生产场景稳定获得延迟与吞吐的双重收益。



点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部