PHP大数组,大文件的处理
[原文来自于转载, 但他的结论不太正确, 尤其对foreach的判断这块上, 我拎过来进行修理 ]
在做数据统计时,难免会遇到大数组,而处理大数据经常会发生内存溢出,这篇文章中,我们聊聊如何处理大数组。
常见的大数组大多来自两种情况:
- 大文件的处理
- DB读取大数据的处理
这里重点讲下DB读取大数据的处理,顺便简单介绍下大文件处理,希望对大家有帮助,看完后可以轻松解决各种大数组问题。
大文件的处理
大家都知道,如果一个文件超过了memory_limit的设置,是不会被加载到内存中的,
试想下假如想要处理一个20G的文件,PHP需要怎么处理呢?
大文件的处理核心思路是:逐行读取。
这样基本可以做到无视文件大小,轻松处理大文件了。
DB读取大数据的处理
从数据库中读取大数据,我们先罗列一下可能会遇到的问题
- 数据量太大无法从数据库中读取
- 大数组无法处理
如果是数据量太大无法从数据库中读取,请优化数据库配置或者优化你的语句,
一般情况是建议优化SQL缩小查询范围,将数据分批进行处理,毕竟DB配置或机器硬件也不能无限优化的。
结论先行,DB读取大数据的处理核心思路是:变量用完及时销毁。
特别是我们循环处理大数组时,是很耗费内存的,所以如果能及时销毁用完的变量,就不用担心内存溢出了。
如何及时销毁变量呢?我们常用的做法可能是用完后销毁,如下
$sql = "your sql";
$rs = $DB->query($sql);
$data = array();
foreach($rs as $v){
//your code
}
unset($rs);//销毁变量
示例中,用完 $rs 之后销毁确实可以释放内存,但实际上大数组的处理中,往往在循环内已经内存溢出无法执行到unset($rs)。那么我们自然而然就想到,能不能在循环内及时销毁用完的变量,是不是也可以及时释放内存呢?答案是可以的。
接下来给大家描述下,我在项目中遇到的问题和解决。
项目实践
项目背景
最近在开发的海外媒体绩效项目,其中有个计算模块,
需根据各个平台的投放数据(注册数、CPA、净收金额等指标)计算每个员工当月的绩效得分,分数用于辅助打绩效。
遇到问题
以5月份为例,从各个统计后台同步到绩效后台的数据量大约有70W+条记录,最极端的情况是,所有的数据都是同一个人投放的,
换句话说,计算这个员工的得分,我需要先从数据库读取这70W+条记录,然后在程序中进行逻辑计算。
在这里就遇到了刚才描述的问题,计算某员工得分,取出的数据量较小的时候,循环顺利执行完毕并且销毁了变量,但当取出的数据量较大的时候,在循环内就已经挂了,执行不到循环处理完毕之后销毁变量。
调试检验
自然而然我也想到了,能不能在foreach循环内,及时销毁用完的变量,从而释放内存呢?思路如下
$sql = "your sql";
$rs = $DB->query($sql);
$data = array();
foreach($rs as $k=>$v){
//your code
unset($rs[$k]);//销毁变量
}
unset($rs);//销毁变量
从结果上看是没有效果的,原来执行不了的仍然是执行不了,我加上些断点和打印信息来辅助排查为什么没有达到预期。
以下是我实际项目去做的测试,代码如下
<?php set_time_limit(0); ini_set('memory_limit', '1024M');//视自身业务情况,这里临时分配足够内存去测试 echo "\r\nstart:" . memory_get_usage(); $res = file_get_contents("./content.txt"); //文件里的是30W行整数 $rs = explode("\n", $res); //这两段可当做是上面的大量的sql查询的结果 echo "\r\nbefore-data:" . memory_get_usage(); $num = count($rs); $sum = 0; foreach($rs as $k=>$value) { //计算实现逻辑 $sum += $value; if($k % 50000 == 0 ){ echo "\r\ncount:".count($rs); echo "\r\nafterMemory:" . memory_get_usage(); } unset($rs[$k]); }
echo "\r\nfinnal-data:" . memory_get_usage();
大约跑了30W条测试数据,输出结果:
[root@07 server]# php mem.php
start:352496
before-data:15926768
count:201121
afterMemory:15926768 @1.
count:151121
afterMemory:24319552 @2
count:101121
afterMemory:24319552
count:51121
afterMemory:24319552
count:1121
afterMemory:24319552
finnal-data: 9490928
从结果上看,@1~@2间要做计算内存持续增长, 程序一般在这里会爆掉 [假如又还没unset语句], 但虽然在循环内$rs数组写明要逐步被UNSET了,但是内存却没有太大变化,没有被释放掉。反而在循环结束后才生效释放出来了, 说明在循环内的unset()必须等循环结束后才会统一回调递归释放掉内存.
但是换种情况循环的时候加引用的话,如下在$value前加个 "&" 符号再来测试发现就可以立即释放内存了,因为&符表引用
foreach($rs as $k=> &$value ) { //计算实现逻辑 $sum += $value; if($k % 50000 == 0 ){ echo "\r\ncount:".count($rs); echo "\r\nafterMemory:" . memory_get_usage(); } unset($rs[$k]); }
[root@07 server]# php mem.php
start:352496
before-data:15926768
count:201121
afterMemory:15926816
count:151121
afterMemory:14326816
count:101121
afterMemory:12726816
count:51121
afterMemory:11126816
count:1121
afterMemory:9526816
finnal-data:9490976
得到的结果完全不同, 在循环内加引用会立即释放内存.
当然也可以使用for循环来得到类似的结果,使用for循环,里边调用unset()的话会立即释放内存.
for ($k=0; $k < $num; $k++) { $value = $rs[$k]; //计算实现逻辑 $sum += $value; if($k % 50000 == 0 ){ echo "\r\ncount:".count($rs); echo "\r\nafterMemory:" . memory_get_usage(); } unset($rs[$k]); }
[root@07 server]# php mem.php
start:352560
before-data:15926832
count:201121
afterMemory:15926832
count:151121
afterMemory:14326832
count:101121
afterMemory:12726832
count:51121
afterMemory:11126832
count:1121
afterMemory:9526832
finnal-data:9490992
从结果可以看出,随着循环的进行,$rs数组逐步被UNSET并且释放了内存,这里涉及到PHP的垃圾回收机制,有兴趣的朋友可以继续深入研究。
至此,DB读取大数据的问题处理完毕。
补充几点小建议
- file_get_contents是一次性把文件内容缓存到内存,相比fgets逐行读取效率要高些,但受限于内存等原因处理大文件时选择逐行读取更合理。
- foreach循环效率高于for循环,譬如for循环每次循环都要判断$i是否小于count,就耗费了一些时间,所以能用foreach就用foreach循环。
- for循环在外部做count比在条件中做count效率更高些,减少了每次循环调用count函数,并且由于处理大数据时会使用unset,导致count($rs)值一直变动,所以for循环在外部做count更合适。
- 为了更好的用户体验,这种大数组处理尽量是定时任务或后台处理
结论: 不管是大文件处理,还是DB读取大数据处理,其实都是用时间换空间,哪种方式更适合,在实际生产中需要依据自身业务的特点去设计。