最近遇到用phpexcel生成excel文件导出,大数据量的时候会遇到PHP内存不足的问题(memory limit)。经测试差不多生成10M左右的excel文件要占用100M左右的内存。解决这个问题准备了三种方案:
调大php的执行内存限制
分批导出excel文件,用php开子进程的方式在每个子进程中处理单个excel的生成,在主进程中用zip打包供用户下载
分批导出excel文件,用php内部调用接口的方式实现,在接口中生成单个excel文件,通过多次调用接口的方式突破内存限制
第一种方案最简单,在代码执行前通过ini_set('limit_memory','1024M')方法来调整内存限制;
第二种方法通过开子进程的方法,只要每个子进程内处理的的单个excel文件不超过内存限制就可以;
第三种方法通过接口实现单个excel的生成,再在主逻辑中调用接口来拼接zip包,循环中调用接口是串行的,所以效率会比较低,不过用curl_multi方法可以实现并行请求,没有实践,预计可行。
主要介绍第二种方法的实现:
安装php的zip扩展和pnctl扩展
-
概念介绍
官方wiki:https://www.php.net/manual/zh/book.pcntl.php
多进程
pcntl_fork — 在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。
pcntl_wait — 挂起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。
伪代码:
//每个excel文件处理2000行数据
$maxCount = 2000;
//带条件的查询
$query = XXXX;
//数据总数
$total = $query->count();
//随机的批次号
$batchId = rand(1000,9999).'-'.date('Ymd-His');
Redis::set('offset_'.$batchId,0);
//zip文件名
$zipFileName = "var/export/" .$batchId . ".zip";
//创建zip对象
$zip = new ZipArchive();
$zip->open($zipFileName,ZipArchive::CREATE);
//存储生成的单个excel文件名
$tempFileArray = [];
//用while循环开多进程
while (Redis::get('offset_'.$batchId) < $total){
//开子进程
$pids = pcntl_fork();
if ($pids == -1) {
//开子进程失败
die('fork error');
} else if ($pids) {
//这里是主进程
pcntl_wait($status); //在子进程结束前挂起主进程,防止出现僵尸进程
} else {
//这里是子进程.子进程会复制父进程中的变量,但是如果想要修改值是不行的,变量不共享(这里的$maxCount,$batchId);
//如果想要共享变量可以使用第三方存储,如redis(这里的$offset);
//获取分页的数据库数据
$offset = Redis::get('offset_'.$batchId);
$collection = $query->limit($maxCount)->offset($offset)->all();
//生成excel文件的逻辑
.....
$excelFileName = 'var/export/'.$batchId.'-'.$offset.'-'.($offset+$maxCount).'.excel';
//添加excel文件到zip包内
$zip->addFile($excelFileName);
}
//添加excel文件名到临时数组(命名规则保持一致,如果在子进程中记录需要把tempFileArray记录到redis中去)
$tempFileArray[] = 'var/export/'.$batchId.'-'.$offset.'-'.($offset+$maxCount).'.excel';
//更新Redis中的offset
$offset += $maxCount;
Redis::set('offset_'.$batchId,$offset);
}
}
$zip->close();
//销毁临时文件
foreach ($tempFileArray as $fileName) {
unlink($fileName);
}
//返回zip包地址
return $zipFileName;
4. 可能遇到的坑
控制总的进程数,防止内存爆掉
for循环内进程是串行执行的,但是为了以防万一的并发问题,如果操作同一个数据表或者文件还是加个队列比较好