PHPExcel 内存溢出解决方法与生成器

最近我们公司做了一个对账平台,我负责做报表这一块。我发现用 PHPExcel 导出5000条以上的数据就出现了这个问题(php.ini 的默认的内存限制是 128M)

1
Allowed memory size of 134217728 bytes exhausted (tried to allocate 54 bytes)

最先想到的是修改 php.ini 的内存限制,但这个方法制标不制本,只能寻找别的解决方法。

下面简单阐述一下我的解决过程

1
2
3
4
5
6
// 获取数据
public function getDetail($map) {
echo memory_get_usage(), '<br>';
$data = M('table')->field('field1,field2,field3,...')->where($map)->order('order_time DESC')->limit(5000)->select();
return $data;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function __construct() {
$this->objPHPExcel = new \PHPExcel();
}
// 遍历 Excel 数据
protected function setSheetData($data) {
echo memory_get_usage(), '<br>';
$activeSheet = $this->objPHPExcel->getActiveSheet();
//导入数据
foreach ($data as $key => $value) {
$index = $key + 2;
$activeSheet->setCellValueExplicit('A'.$index, $value['field1'], \PHPExcel_Cell_DataType::TYPE_STRING);
$activeSheet->setCellValueExplicit('B'.$index, $value['field2'], \PHPExcel_Cell_DataType::TYPE_STRING);
$activeSheet->setCellValue('C'.$index, $value['field3']);
...
}
echo 'foreach: ' . memory_get_usage(), '<br>';
exit;
}

以下测试数据都是基于导出 5000 条数据产生的

过程 查询数据前 查询数据后 遍历数据
优化前 6M 26M 70M

通过 php 函数 memory_get_usage 可以获取到如上数据。可以发现,内存占用大的情况出现在查询后和遍历 Excel 数据的时候。

PHPExcel 大数据量情况下内存溢出解决

PHPExcel 可以通过缓存来解决这种情况,大家可以参考这篇 文章 来解决遍历数据那一步

1
2
3
4
5
6
public function __construct() {
$cacheMethod = \PHPExcel_CachedObjectStorageFactory::cache_in_memory_gzip;
$cacheSettings = array('cache_in_memory_gzip'=>'30MB');
\PHPExcel_Settings::setCacheStorageMethod($cacheMethod, $cacheSettings);
$this->objPHPExcel = new \PHPExcel();
}
过程 查询数据前 查询数据后 遍历数据
优化后 6M 26M 51M

注销遍历完的变量

在上面的代码中,$data 是存了 5000 条数据的变量,占用了较大的内存。我们遍历完数据后这个变量也就没用了,所以我们可以用 unset() 来的注销这个变量。
但是重点在下文的生成器,就算不用 unset() 也没关系了。

过程 查询数据前 查询数据后 遍历数据 注销变量
优化后 6M 26M 51M 35M

应用 PHP 的生成器

官方注释:生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

我最近在学习 python,看到了生成器这部分,想起了 PHP 也有生成器。我们不可能把成千上万条数据存到变量中,这样内存一下子就达到上限了。但我们可以利用生成器,代码如下:

1
2
3
4
5
6
7
8
public function getDetail($map) {
$sql = "SELECT `field1`,`field2`,`field3` ... FROM `table` LIMIT 5000";
$conn = mysqli_connect('127.0.0.1', 'root', '', 'database');
$res = mysqli_query($conn, $sql);
while ($row = mysqli_fetch_assoc($res)) {
yield $row;
}
}
过程 查询数据前 查询数据后 遍历数据
最初 6M 26M 70M
最终 6M 6M 31M

经过测试,查询数据前后的内存占用基本不变。经过上面几步的优化,遍历数据那一步现在的内存占用也只有 31M 了。就算是 php.ini 默认的 128M 的内存也足够用了。关于生成器的详细介绍,大家可以去看 PHP 官方手册。
我发现网上在解决这个问题没有用上生成器,所以便有了这篇文章。解决了这类问题还是满有成就感的,嘿嘿!