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
相关产品推荐
相关产品推荐

