PhpSpreadsheet读取大Xlsx文件缓存配置报错求助
问题背景
我需要读取一个包含40000行数据的大型Xlsx文件,在旧版本PHPExcel中使用缓存功能可正常运行。现迁移至最新版PhpSpreadsheet,必须配置缓存,否则程序会触发内存分配错误(已在php.ini中设置memory_limit = 5000M),错误信息如下:
Fatal error: Out of memory (allocated 780140544) (tried to allocate 29360128 bytes) in D:*\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Collection\Cells.php on line 400
尝试的缓存配置
我尝试了APCu和Redis两种缓存组件,配置代码如下:
$client = new \Redis(); $client->connect('127.0.0.1', 6379); $pool = new \Cache\Adapter\Redis\RedisCachePool($client); // $pool = new \Cache\Adapter\Apcu\ApcuCachePool(); $simpleCache = new \Cache\Bridge\SimpleCache\SimpleCacheBridge($pool); \PhpOffice\PhpSpreadsheet\Settings::setCache($simpleCache); $objReader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader("Xlsx"); $objReader->setReadDataOnly(true); $objPHPExcel = $objReader->load(dirname(__FILE__).'/Tmpfile'.$i.'.xlsx'); $objPHPExcel->setActiveSheetIndex(0); foreach ( $objPHPExcel->getActiveSheet()->getRowIterator() as $row ) { // 业务逻辑 }
新出现的错误
但两种配置均触发致命错误:
APCu缓存错误
APCu: Fatal error: Uncaught PhpOffice\PhpSpreadsheet\Exception: Cell entry A2 no longer exists in cache. This probably means that the cache was cleared by someone else. in D:\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Collection\Cells.php:433 Stack trace: #0 D:\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Worksheet\Worksheet.php(1239): PhpOffice\PhpSpreadsheet\Collection\Cells->get('A2') #1 D:\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Worksheet\RowCellIterator.php(128): PhpOffice\PhpSpreadsheet\Worksheet\Worksheet->getCellByColumnAndRow(1, 2) #2 D:\Eclipse\WebShopUpdate\run.php(358): PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator->current() #3 {main} thrown in D:*\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Collection\Cells.php on line 433
Redis缓存错误
Redis: Fatal error: Uncaught RedisException: Redis server went away in D:\cache\redis-adapter\RedisCachePool.php:82 Stack trace:
0 D:\cache\redis-adapter\RedisCachePool.php(82): Redis->set('phpspreadsheet....', 'a:4:{i:0;b:1;i:...') #1
D:\cache\adapter-common\AbstractCachePool.php(240): Cache\Adapter\Redis\RedisCachePool->storeItemInCache(Object(Cache\Adapter\Common\CacheItem), NULL) #2 D:\cache\simple-cache-bridge\SimpleCacheBridge.php(72): Cache\Adapter\Common\AbstractCachePool->save(Object(Cache\Adapter\Common\CacheItem))
3 D:\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Collection\Cells.php(372):
Cache\Bridge\SimpleCache\SimpleCacheBridge->set('phpspreadsheet....', Object(PhpOffice\PhpSpreadsheet\Cell\Cell)) #4 D:\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Collection\Cells.php(398): PhpOffice\PhpSpr in D:*\cache\adapter-common\AbstractCachePool.php on line 337
运行环境
环境1
- PHP Version 7.1.3
- Apache/2.4.25 (Win32)
- APCu Version 5.1.11
- Redis Version 4.0.2
环境2
- PHP Version 5.6.30
- Apache/2.4.25 (Win32)
- APCu Version 4.0.10
- Redis Version 2.2.7
两种环境下均出现相同错误,恳请解决该问题。
解决方案
先解决APCu缓存的问题
这个“Cell entry no longer exists”的错误,说白了就是缓存里的单元格数据丢了,大概率是APCu的缓存生命周期或者空间不够导致的,咱们试试这几个办法:
- 调大APCu的缓存参数:打开
php.ini,把apc.ttl设成3600(1小时足够处理大文件了),apc.shm_size改成128M甚至更高,避免缓存被提前清理或者空间不够。 - 给PhpSpreadsheet的缓存加专属命名空间:默认缓存键没有隔离,很容易和其他应用的缓存冲突。咱们给缓存池加个唯一的命名空间,确保不会被误清:
$pool = new \Cache\Adapter\Apcu\ApcuCachePool(); // 用uniqid生成唯一命名空间,避免重复 $pool->setNamespace('phpspreadsheet_' . uniqid()); $simpleCache = new \Cache\Bridge\SimpleCache\SimpleCacheBridge($pool); \PhpOffice\PhpSpreadsheet\Settings::setCache($simpleCache);
- 暂时禁用其他缓存清理操作:如果服务器上有定时脚本或者其他程序会清理APCu缓存,先暂时关掉,等处理完Excel文件再开。
再搞定Redis的连接问题
“Redis server went away”一般是连接断了,要么是超时,要么是数据太大扛不住,试试这些调整:
- 优化Redis连接参数:给连接加超时时间,开持久连接,避免处理大文件时连接被断开:
$client = new \Redis(); // 30秒超时,持久连接标识设成唯一的 $client->connect('127.0.0.1', 6379, 30, 'phpspreadsheet_conn'); // 如果Redis设了密码,记得加上认证 // $client->auth('your_redis_pass'); $pool = new \Cache\Adapter\Redis\RedisCachePool($client);
- 调整Redis服务器配置:打开
redis.conf,把tcp-keepalive改成300,防止连接被主动回收;同时把maxmemory设得足够大,别让Redis因为内存不够把缓存数据删了。 - 换个更高效的序列化方式:默认的序列化效率低,试试用
igbinary扩展(先在PHP里装好这个扩展),然后让Redis用它来序列化数据:
$client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY);
通用的优化小技巧
不管用哪种缓存,这些操作都能帮你减少内存压力:
- 分批读取数据:别一次性把4万行都加载进来,用
IReadFilter分批读,比如一次读1000行,处理完一批就清理缓存和对象:
class ChunkReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { private $startRow; private $endRow; public function __construct($startRow, $chunkSize) { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; } public function readCell($column, $row, $worksheetName = '') { return $row >= $this->startRow && $row < $this->endRow; } } // 先拿到总行数 $worksheetInfo = $objReader->listWorksheetInfo()[0]; $totalRows = $worksheetInfo['totalRows']; // 分批读取处理 for ($startRow = 1; $startRow <= $totalRows; $startRow += 1000) { $filter = new ChunkReadFilter($startRow, 1000); $objReader->setReadFilter($filter); $spreadsheet = $objReader->load(dirname(__FILE__).'/Tmpfile'.$i.'.xlsx'); // 处理当前批次的行数据 $sheet = $spreadsheet->getActiveSheet(); foreach ($sheet->getRowIterator() as $row) { // 你的业务逻辑 } // 手动清理,释放内存 unset($spreadsheet, $sheet); gc_collect_cycles(); }
- 更新PhpSpreadsheet到最新版:旧版本可能有缓存相关的bug,用
composer update phpoffice/phpspreadsheet更到最新稳定版试试。 - 关闭无用功能:除了
setReadDataOnly(true),再加个$objReader->setReadEmptyCells(false);,不读空单元格,减少缓存的数据量。
内容的提问来源于stack exchange,提问作者Ekhrikhor

