Laravel迁移使用chunk仍报内存限制错误的修复方案
内存溢出原因
首先你提到已使用cursor游标,但贴出的代码中仅调用了chunk()方法,并没有使用游标查询,出现内存耗尽的核心原因有4个:
- 分块逻辑存在缺陷:普通
chunk()基于偏移量实现分页,你当前查询加了连表、geometry非空过滤条件,没有显式指定主键排序规则,会导致数据库每次执行分页查询时都要扫描全量符合条件的结果做偏移,数据库连接端缓存的结果集持续占用内存,处理越往后内存占用越高。 - 事务粒度过大:你把单批1000条的所有插入逻辑全部包裹在同一个数据库事务中,数据库驱动会将事务生命周期内所有待执行的语句、返回结果全部缓存在内存中直到事务提交。而geometry字段本身是体积较大的空间GeoJSON数据,1000条的事务缓存很容易达到数百MB,多轮处理后内存持续累积就会触碰上限。
- 内存回收不及时:循环中通过GeoJson库反序列化生成的对象、json编码生成的字符串,以及ORM查询返回的模型实例,如果存在隐式的引用持有(比如模型事件监听器、全局作用域持有引用),PHP的GC无法及时回收每轮分块处理完的内存,内存占用会随处理进度线性上涨。
- 单块大小设置不合理:每条记录都携带大体积的geometry字段,单批1000条的原始数据本身就会占用很高内存,加上ORM类型自动转换、处理过程中生成的中间变量,单批内存峰值很容易超过预期。
修复方案
按照以下步骤调整即可解决内存溢出问题:
- 改用
chunkById()替代普通chunk(),该方法基于主键游标实现分页,不会出现偏移量分页的全表扫描问题,同时显式指定查询按stationary源表的主键升序排列,避免连表导致的分页逻辑异常。 - 调小分块大小到200,缩小单批数据的内存峰值;调整事务粒度,你的插入逻辑加了
on conflict do nothing本身是幂等操作,可以直接去掉大事务,减少数据库驱动的事务缓存占用。 - 查询时加
toBase()直接返回标准对象,不实例化Eloquent模型,能砍掉一半以上的ORM内存开销;每处理完单条记录后手动释放无用变量,每处理完一个分块手动触发GC回收内存。 - 避免直接把变量拼接进SQL语句,改用参数绑定防止意外的语法问题,同时减少字符串拼接的内存开销。
修复后的可运行代码:
<?php use Rpn\Services\Onv\Models\OnvForm\EmissionsStationarySource; use Rpn\Services\Map\Models\MapLayer; use GeoJson\GeoJson; use Illuminate\Support\Facades\DB; EmissionsStationarySource::select("onvos_request_emissions_stationary_sources.*") ->toBase() // 直接返回stdClass,不实例化Eloquent模型,大幅降低内存占用 ->join('onvs', function ($join) { $join->on('onvs.service_request_id', '=', 'onvos_request_emissions_stationary_sources.service_request_id'); }) ->whereNotNull('geometry') ->orderBy('onvos_request_emissions_stationary_sources.id') ->chunkById(200, function ($stationaries) { $layer = MapLayer::MAP_LAYER_STATIONARY; $type = EmissionsStationarySource::class; foreach ($stationaries as $stationary) { $id = $stationary->id; if (empty($stationary->geometry)) { continue; } try { // 验证GeoJSON格式合法性 $point = GeoJson::jsonUnserialize($stationary->geometry); $geo = json_encode($stationary->geometry); } catch (\Throwable $e) { continue; } // 改用参数绑定,避免SQL语法问题和字符串拼接开销 DB::statement(" INSERT INTO map_objects(map_layer_id, model_type, model_id, geometry, created_at, updated_at) VALUES (?, ?, ?, ST_MakeValid(ST_GeomFromGeoJSON(?) ), now(), now()) ON CONFLICT DO NOTHING; ", [$layer, $type, $id, $geo]); // 手动释放单条记录的中间变量 unset($geo, $point); } // 分块处理完手动触发GC回收 gc_collect_cycles(); }, 'onvos_request_emissions_stationary_sources.id');
如果是一次性数据同步脚本,调整后仍有内存压力,可以在脚本开头临时调整内存限制:
ini_set('memory_limit', '4G');,但优先通过上述逻辑优化降低内存占用,不要盲目调大内存上限。
内容的提问来源于stack exchange,提问作者Alexander Orlovskiy
相关产品推荐
相关产品推荐

