You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel 8中chunk处理大量数据时内存泄漏问题求助

Laravel Artisan命令处理大量数据时内存持续上涨问题

我有一个每15分钟运行一次的Artisan命令,用于更新模型的计算字段。该命令需处理数千条数据,但最终会耗尽所有内存。

根据Laravel文档的理解,使用chunk方法可大幅降低遍历大量数据时的内存占用,因为它每次仅在内存中保留N条数据而非整个数据集。我已使用该方法,但每处理完一个chunk,Docker容器的内存占用仍持续增加。

原始代码:

public function __construct()
{
    parent::__construct();
    $this->openingHourArraySerialiser = new OpeningHourArraySerializer();
}

public function handle()
{
    $this->openingHours = OpeningHour::with('scheduleHours')->get();
    ini_set('memory_limit', '1024M');
    $this->getTicketQuery()->chunk(500, function($tickets) {
        $cases = [];
        $ids = [];
        $params = [];
        foreach($tickets as $ticket) {
            $cases[] = "WHEN {$ticket->ID} then ?";
            $openingHourSchedule = $this->getOpeningHoursSchedule($ticket);
            $params[] = TicketPriorityLevelCalculator::calculate($ticket, $openingHourSchedule);
            $ids[] = $ticket->ID;
        }
        $ids = implode(',', $ids);
        $cases = implode(' ', $cases);
        if (!empty($ids)) {
            DB::connection('tcollect')->update("UPDATE _DSKTickets SET PriorityLevel = CASE ID {$cases} END WHERE ID in ({$ids})", $params);
        }
    });
    $this->setTicketWithoutDueDatePriority();
}

private function getOpeningHoursSchedule($ticket)
{
    $openingHours = $this->openingHours->where('ID', $ticket->OpeningHoursID)->first();
    return $openingHours->getSerializedSchedule($this->openingHourArraySerialiser);
}

我已尝试禁用查询日志、设置APP_DEBUG为false,还试过使用chunkById、lazy、lazyById方法,以及手动unset变量,但内存占用情况依旧。由于需要预加载关联关系,无法使用cursor方法。

如何将内存占用控制在1GB以内?


解决方案

1. 取消全局预加载OpeningHour,改为按需查询/缓存

原代码中$this->openingHours = OpeningHour::with('scheduleHours')->get();一次性加载所有数据并长期持有,是内存占用的核心来源之一。替换为按需查询,或缓存序列化后的结果:

按需查询版本:

private function getOpeningHoursSchedule($ticket)
{
    // 仅查询当前ticket需要的OpeningHour及其关联
    $openingHour = OpeningHour::with('scheduleHours')
        ->where('ID', $ticket->OpeningHoursID)
        ->first();
    return $openingHour->getSerializedSchedule($this->openingHourArraySerialiser);
}

缓存版本(适合OpeningHour不频繁更新的场景):

use Illuminate\Support\Facades\Cache;

private function getOpeningHoursSchedule($ticket)
{
    $cacheKey = "opening_hour_schedule_{$ticket->OpeningHoursID}";
    // 缓存1小时,根据数据更新频率调整有效期
    return Cache::remember($cacheKey, 3600, function () use ($ticket) {
        $openingHour = OpeningHour::with('scheduleHours')
            ->where('ID', $ticket->OpeningHoursID)
            ->first();
        return $openingHour->getSerializedSchedule($this->openingHourArraySerialiser);
    });
}

2. 强制清理chunk内的变量与模型引用

Laravel模型实例可能被内部机制持有引用,导致无法被垃圾回收。在chunk闭包末尾手动清理并触发回收:

$this->getTicketQuery()->chunkById(200, function($tickets) { // 缩小chunk size到200
    $cases = [];
    $ids = [];
    $params = [];
    foreach($tickets as $ticket) {
        $cases[] = "WHEN {$ticket->ID} then ?";
        $openingHourSchedule = $this->getOpeningHoursSchedule($ticket);
        $params[] = TicketPriorityLevelCalculator::calculate($ticket, $openingHourSchedule);
        $ids[] = $ticket->ID;
        // 单个ticket处理完立即释放
        unset($ticket);
    }
    $ids = implode(',', $ids);
    $cases = implode(' ', $cases);
    if (!empty($ids)) {
        DB::connection('tcollect')->update("UPDATE _DSKTickets SET PriorityLevel = CASE ID {$cases} END WHERE ID in ({$ids})", $params);
    }
    // 清理当前chunk的所有变量
    unset($cases, $ids, $params, $tickets);
    // 强制触发垃圾回收
    gc_collect_cycles();
});

3. 优化查询语句,只选择需要的字段

修改getTicketQuery(),仅查询计算所需的字段,避免加载不必要的模型数据:

private function getTicketQuery()
{
    // 只选择ID和OpeningHoursID(计算PriorityLevel必需的字段)
    return Ticket::select('ID', 'OpeningHoursID')
        ->where(...); // 保留原查询条件
}

4. 关闭不必要的Laravel功能

在handle方法开头关闭模型事件监听(如果不需要),减少内存占用:

public function handle()
{
    // 关闭模型事件,避免持有模型引用
    Ticket::unsetEventDispatcher();
    OpeningHour::unsetEventDispatcher();
    
    ini_set('memory_limit', '1024M');
    // ... 后续逻辑 ...
}

5. 清理类属性中的大对象

如果必须预加载OpeningHour,处理完chunk后立即释放:

public function handle()
{
    $openingHours = OpeningHour::with('scheduleHours')->get();
    $this->openingHours = $openingHours;
    
    // ... chunk处理逻辑 ...
    
    // 处理完chunk后立即释放内存
    unset($this->openingHours, $openingHours);
    gc_collect_cycles();
    
    $this->setTicketWithoutDueDatePriority();
}

内容的提问来源于stack exchange,提问作者naghal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 00:36:22