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

Laravel批量压缩大量PDF文件如何避免PHP内存限制错误?

大文件批量压缩内存优化方案

核心问题分析

原代码内存占用过高的核心原因:

  • Storage::get($pdf_path) 将整个PDF文件加载到内存,500个批次累积后内存占用急剧攀升
  • 一次性加载整个批次的Ticket数据到集合,即使手动unset也可能因PHP引用计数问题无法及时释放内存
  • 临时Zip文件的二次拷贝(先创建本地文件再复制到storage)额外占用IO和内存资源

具体优化措施

  • 用ZipArchive::addFile直接添加文件:跳过内存读取步骤,直接从磁盘路径将文件加入压缩包,彻底避免PDF文件占用内存
  • 使用Laravel游标查询(cursor):惰性加载Ticket数据,每次仅在处理时加载单条记录,内存占用维持在极低水平
  • 直接在目标路径创建Zip文件:避免临时文件的创建与拷贝流程,减少不必要的IO和内存开销
  • 临时关闭模型非必要特性:关闭模型事件、观察者等功能,减少额外内存消耗

修改后的完整代码

if ($this->argument('year')) {
    $startDate = Carbon::createFromDate($this->argument('year'), $this->argument('month'), 1)->startOfMonth()->startOfDay()->toImmutable();
    $endDate = $startDate->endOfMonth()->endOfDay()->toImmutable();

    $this->info('Fetching the tickets between '.$startDate->format('Y-m-d H:i:s').' and '.$endDate->format('Y-m-d H:i:s'));
    $batchSize = 500;
    $ticketsCount = Ticket::where('created_at', '>=', $startDate)
        ->where('created_at', '<=', $endDate)
        ->count();
    $this->line('<fg=blue>Total Tickets: '.$ticketsCount.'</>');

    if ($ticketsCount > 0) {
        $this->line("\n<fg=yellow>Start building the .zip file.</>");
        $pdfCounter = 0;
        $totalBatches = ceil($ticketsCount / $batchSize);
        DB::connection()->disableQueryLog();
        
        // 临时关闭模型事件,减少内存开销
        Ticket::unsetEventDispatcher();

        for ($batch = 1; $batch <= $totalBatches; $batch++) {
            $offset = ($batch - 1) * $batchSize;
            Log::info('Batch: ' . $batch . ', Offset: ' . $offset);

            // 直接在storage目标路径创建Zip,避免临时文件操作
            $zipFilename = env('APP_ENV').'_tickets_archive_'.$this->argument('year').'_'.$this->argument('month').'_batch'.$batch.'.zip';
            $zipPath = storage_path('app/ticket-archive/'.$zipFilename);
            $zip = new \ZipArchive();
            $zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);

            // 使用cursor惰性加载,逐个处理Ticket记录
            $ticketsCursor = Ticket::where('created_at', '>=', $startDate)
                ->where('created_at', '<=', $endDate)
                ->offset($offset)
                ->limit($batchSize)
                ->select('number', 'pdf_path')
                ->cursor();

            $bar = $this->output->createProgressBar($batchSize);

            foreach ($ticketsCursor as $ticket) {
                $pdfFullPath = Storage::path($ticket->pdf_path);
                if (file_exists($pdfFullPath)) {
                    // 直接添加磁盘文件到Zip,不加载到内存
                    $zip->addFile($pdfFullPath, $ticket->number . '.pdf');
                    $pdfCounter++;
                }
                $bar->advance();
                // 手动销毁模型实例,释放内存
                $ticket->__destruct();
                unset($ticket);
            }

            $bar->finish();
            unset($bar, $ticketsCursor);
            gc_collect_cycles();

            // 输出内存占用日志
            $this->line("\n<fg=yellow>Memory usage peak: " . number_format(memory_get_peak_usage(), 0, '.', ',') . " bytes</>");
            $this->line("<fg=yellow>Memory usage end: " . number_format(memory_get_usage(), 0, '.', ',') . " bytes</>");

            if ($zip->numFiles > 0) {
                $zip->close();
                $this->line("\n<fg=green>Zip file created: {$zipPath}</>");
            } else {
                $this->line(nl2br("<fg=red>No files found in batch {$batch}.</>"));
                $zip->close();
                unlink($zipPath);
            }

            $this->line("\n<fg=blue>Total files in this batch: ".$pdfCounter."</>");
            $pdfCounter = 0;
            $this->line('');
            
            DB::flushQueryLog();
            gc_collect_cycles();

            // 输出批次清理后的内存日志
            $this->line("\n<fg=yellow>Memory usage after batch cleanup: " . number_format(memory_get_usage(), 0, '.', ',') . " bytes</>");
        }
        
        // 恢复模型事件
        Ticket::setEventDispatcher(app('events'));
    }
}

额外优化建议

  • 调整批次大小:如果单批次内存仍过高,可将batchSize降至100-200进一步降低内存峰值
  • 用memory_get_usage(true)查看真实内存占用:该参数会返回PHP实际从系统分配的内存大小,更贴近真实消耗
  • 减少循环内日志写入:高频日志会增加IO开销,可改为批次结束后统一记录关键信息

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 10:42:55