PhpSpreadsheet加载大文件后内存无法释放问题求助
解决PhpSpreadsheet读取大Excel时内存泄漏导致Apache崩溃的问题
在Linux Apache环境的Laravel项目中,使用PhpSpreadsheet读取包含5万+行数据的超大Excel时,进程内存占用可达数GB。即便按照官方指引执行$spreadsheet->disconnectWorksheets();和unset($spreadsheet);,内存仍无法释放,最终导致服务器崩溃。以下是当前使用的读取方法:
public static function readFile($file, string $sheet = null): array { $bookarray = array(); $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->setReadDataOnly(true); $spreadsheet = $reader->load($file); if ($sheet != null) { $worksheet = $spreadsheet->getSheetByName($sheet); if ($worksheet == null) { $worksheet = $spreadsheet->getActiveSheet(); } } else { $worksheet = $spreadsheet->getActiveSheet(); } foreach ($worksheet->getRowIterator() as $row) { $rowarray = array(); $cellIterator = $row->getCellIterator(); foreach ($cellIterator as $cell) { array_push($rowarray, $cell->getValue()); } array_push($bookarray, $rowarray); unset($rowarray); unset($cellIterator); } $bookarray = array_values($bookarray); $spreadsheet->disconnectWorksheets(); unset($spreadsheet); unset($worksheet); unset($reader); return $bookarray; }
核心问题分析
当前代码一次性将整个Excel的所有行加载到内存中的$bookarray,即便销毁PhpSpreadsheet相关变量,这个大数组依然占用大量内存,直到请求结束。此外,PHP的垃圾回收机制不会立即释放内存,Apache的进程复用机制会导致内存无法及时回收。
解决方案
1. 分块读取Excel(使用Read Filter)
不要一次性加载整个文件,而是通过自定义Read Filter分批次读取行数据,大幅降低内存峰值:
class ChunkReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { private $startRow = 0; private $endRow = 0; public function setRows($startRow, $chunkSize) { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; } public function readCell($column, $row, $worksheetName = '') { if ($row >= $this->startRow && $row < $this->endRow) { return true; } return false; } } public static function readLargeFile($file, string $sheet = null, $chunkSize = 1000) { $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->setReadDataOnly(true); $filter = new ChunkReadFilter(); $reader->setReadFilter($filter); // 获取总行数 $spreadsheet = $reader->load($file); if ($sheet !== null) { $worksheet = $spreadsheet->getSheetByName($sheet) ?? $spreadsheet->getActiveSheet(); } else { $worksheet = $spreadsheet->getActiveSheet(); } $totalRows = $worksheet->getHighestRow(); $spreadsheet->disconnectWorksheets(); unset($spreadsheet); // 分块处理 for ($startRow = 1; $startRow <= $totalRows; $startRow += $chunkSize) { $filter->setRows($startRow, $chunkSize); $spreadsheet = $reader->load($file); $worksheet = $spreadsheet->getSheetByName($sheet) ?? $spreadsheet->getActiveSheet(); foreach ($worksheet->getRowIterator($startRow, min($startRow + $chunkSize - 1, $totalRows)) as $row) { $rowarray = []; foreach ($row->getCellIterator() as $cell) { $rowarray[] = $cell->getValue(); } // 这里直接处理行数据,比如写入数据库,不要存到大数组里 // processRow($rowarray); } $spreadsheet->disconnectWorksheets(); unset($spreadsheet, $worksheet); gc_collect_cycles(); // 强制触发垃圾回收 } }
2. 避免返回完整数据集
如果业务不需要一次性获取所有行数据,直接在循环内处理每行(如写入数据库),不要将所有行存储到$bookarray中返回,这是内存占用过高的主要原因之一。
3. 强制触发垃圾回收
在销毁变量后手动调用gc_collect_cycles(),强制PHP回收循环引用的内存(PhpSpreadsheet对象存在大量循环引用)。
4. 调整Apache/PHP运行模式
- 改用PHP-FPM替代Apache模块运行PHP:每个请求结束后,FPM进程会销毁,内存自动释放,避免Apache进程复用导致的内存累积。
- 如果坚持用Apache模块,调整
MaxRequestWorkers、MaxConnectionsPerChild等参数,限制子进程的请求次数和内存占用,让Apache定期回收进程。
5. 代码细节优化
- 替换
array_push为直接赋值:$rowarray[] = $cell->getValue()比array_push更高效。 - 减少变量作用域:循环内的变量尽量在循环内定义,避免全局作用域残留。
内容的提问来源于stack exchange,提问作者Tomas De pamphilis
相关产品推荐
相关产品推荐

