内存溢出主因是collect()/get()全量加载模型致内存暴涨,应改用db::cursor()流式读取或chunkbyid()分页处理,并关闭事件、清理查询日志、复用连接。

内存溢出不是 PHP 报错说“Allowed memory size exhausted”就完事了——它背后往往是 collect() 或 get() 拉全量数据进内存,再套个 foreach 循环,对象没释放、查询缓存没清、模型事件还在监听,结果 10 万条记录还没跑完,PHP 进程就跪了。
为什么 collect() + foreach 会爆内存
很多人以为 collect() 是轻量封装,其实它把整个 Eloquent 集合(含所有模型实例、关系、属性访问器、变更跟踪器)全载入内存。每条 Model 实例平均占 2–5KB,10 万条就是 200MB+,还不算中间变量和闭包引用。
-
collect(Model::all())等价于先get()再包装,完全没避开加载瓶颈 - 循环中调用
$model->relation触发懒加载,又引入 N+1 查询 + 新模型实例 - 没手动
unset($model)或脱离作用域,PHP GC 不会立刻回收(尤其有闭包或事件监听时)
chunk() 不是万能的,但必须用对
chunk() 的本质是分页式 SQL 查询(LIMIT + OFFSET),但它只解决“一次不拉太多”,不解决“拉进来后怎么处理”。常见误用:
-
Model::chunk(1000, function ($models) { $models->each(...); })——each()仍是全量遍历这批模型,内存峰值仍高 - 在 chunk 回调里调用
$model->load('relation'),导致每批都额外查关联表,拖慢速度还涨内存 - 忘记加
->withoutEvents(),每个模型创建/更新都触发 Observer 和 Event,开销翻倍
正确姿势是:用 chunkById()(避免 OFFSET 越往后越慢),配合 select() 明确字段,必要时 DB::table() 绕过模型。
真正低内存的循环写法:DB::cursor() + yield
Laravel 9+ 提供 DB::cursor(),它不构建集合、不实例化模型,而是逐行从 PDO Statement 流式读取原始数组,内存占用恒定在 KB 级别。
- 适合只读场景(如导出、统计、清洗):直接处理
['id' => 1, 'name' => '...'] - 配合
yield可进一步做成生成器,避免中间数组累积:foreach (yourGenerator() as $row) { ... } - 若需模型逻辑,用
new Model($row)手动构造,用完立刻unset,不走create()或fill()全生命周期
示例:
foreach (DB::table('users')->select('id', 'email')->cursor() as $row) {
// 处理单行,无模型开销
sendWelcomeEmail($row['email']);
// 不需要 unset($row),for 循环自动释放
}
容易被忽略的释放点:事件、连接、查询构建器
即使你用了 chunk() 或 cursor(),以下三处仍可能悄悄吃光内存:
-
Model::observe()或全局事件监听器未关闭:用Model::withoutEvents(fn() => {...})包裹整块逻辑 - 长时间运行的命令没重置查询构建器缓存:
DB::flushQueryLog()和Model::flushEventListeners()在批次末尾调用 - 数据库连接未复用或泄露:CLI 命令默认长连接,用
DB::reconnect()或DB::disconnect()控制生命周期
最危险的是“以为自己已经很省了”,结果在循环里调了一次 Cache::remember() 或 Storage::get(),缓存键拼错导致全量加载,或者文件内容被读进内存没 unset——这种细节比框架选择更致命。











