时间换空间-PHP大数组处理

导语

在做数据统计时,难免会遇到大数组,而处理大数据经常会发生内存溢出,这篇文章中,我们聊聊如何处理大数组。

情况分类

常见的大数组大多来自两种情况:

大文件的处理

DB读取大数据的处理

这里重点讲下DB读取大数据的处理,顺便简单介绍下大文件处理,希望对大家有帮助,看完后可以轻松解决各种大数组问题。

大文件的处理

大家都知道,如果一个文件超过了memory_limit的设置,是不会被加载到内存中的,

试想下假如想要处理一个20G的文件,PHP需要怎么处理呢?

这样基本可以做到无视文件大小,轻松处理大文件了。

DB读取大数据的处理

从数据库中读取大数据,我们先罗列一下可能会遇到的问题

数据量太大无法从数据库中读取

大数组无法处理

如果是数据量太大无法从数据库中读取,请优化数据库配置或者优化你的语句,

一般情况是建议优化SQL缩小查询范围,将数据分批进行处理,毕竟DB配置或机器硬件也不能无限优化的。

特别是我们循环处理大数组时,是很耗费内存的,所以如果能及时销毁用完的变量,就不用担心内存溢出了。

如何及时销毁变量呢?我们常用的做法可能是用完后销毁,如下

$sql ="your sql";

$rs = $DB->query($sql);

$data = array();

foreach($rsas$v){

//your code

}

unset($rs);//销毁变量

示例中,用完 $rs 之后销毁确实可以释放内存,但实际上大数组的处理中,往往在循环内已经内存溢出无法执行到unset($rs)。那么我们自然而然就想到,能不能在循环内及时销毁用完的变量,是不是也可以及时释放内存呢?答案是可以的。

接下来给大家描述下,我在项目中遇到的问题和解决。

项目实践

项目背景

最近在开发的海外媒体绩效项目,其中有个计算模块,

需根据各个平台的投放数据(注册数、CPA、净收金额等指标)计算每个员工当月的绩效得分,分数用于辅助打绩效。

遇到问题

以5月份为例,从各个统计后台同步到绩效后台的数据量大约有70W+条记录,最极端的情况是,所有的数据都是同一个人投放的,

换句话说,计算这个员工的得分,我需要先从数据库读取这70W+条记录,然后在程序中进行逻辑计算。

在这里就遇到了刚才描述的问题,计算某员工得分,取出的数据量较小的时候,循环顺利执行完毕并且销毁了变量,但当取出的数据量较大的时候,在循环内就已经挂了,执行不到循环处理完毕之后销毁变量。

调试检验

自然而然我也想到了,能不能在foreach循环内,及时销毁用完的变量,从而释放内存呢?思路如下

$sql ="your sql";

$rs = $DB->query($sql);

$data = array();

foreach($rsas$k=>$v){

//your code

unset($rs[$k]);//销毁变量

}

unset($rs);//销毁变量

从结果上看是没有效果的,原来执行不了的仍然是执行不了,我加上些断点和打印信息来辅助排查为什么没有达到预期。

以下是我实际项目去做的测试,代码如下

set_time_limit();

ini_set('memory_limit','1024M');//视自身业务情况,这里临时分配足够内存去测试

echo"\r\nbefore-sql:". memory_get_usage();

$sql ="my sql";

$rs = M()->query($sql);

$data = array();

echo"\r\nbefore-data:". memory_get_usage();

foreach ($rsas$k => $value) {

//计算实现逻辑

...

//员工数据初步汇总

$data[$user_id][$key]['reg'] += $value['reg_num'];

$data[$user_id][$key]['fee_money'] += $value['ad_fee_usd'];

$data[$user_id][$key]['pay_money'] += $value['usd_money'];

echo"\r\nmid-before-arr:". memory_get_usage();

print_r($rs[1]);

unset($rs[$k]);

echo"\r\narr-count:".count($rs);

echo"\r\nmin-finnal-arr:". memory_get_usage();

}

echo"\r\nfinnal-data:". memory_get_usage();

大约跑了30W条测试数据,输出结果:

before-sql:3483192

before-data:917716608

mid-before-arr:917727168

Array

(

...

[reg_num] => 2

[ad_fee_rmb] => 3.17

)

arr-count:298790

min-finnal-arr:948215152

mid-before-arr:948215152

Array

(

...

[reg_num] => 2

[ad_fee_rmb] => 3.17

)

arr-count:298789

min-finnal-arr:948215064

...

mid-before-arr:1009418440

arr-count:3340

min-finnal-arr:1009418632

finnal-data:289371536

从结果上看,在循环内$rs数组确实逐步被UNSET了,但是内存却没有太大变化,没有被释放掉。

查阅了相关资料,原来是在foreach内UNSET当前循环的数组信息不会影响数组中的键值, 只有当本数组结束后UNSET的值才会被真正的释放掉。

问题解决

既然foreach循环内不能释放内存,我可以换一种循环方法,测试使用for循环。

跟刚才一样的测试数据,输出结果:

before-sql:3483400

before-data:917721344

mid-before-arr:917732040

Array

(

...

[reg_num] => 2

[ad_fee_rmb] => 3.17

)

arr-count:298790

min-finnal-arr:917731952

mid-before-arr:917729632

Array

(

...

[reg_num] => 2

[ad_fee_rmb] => 3.17

)

arr-count:298789

min-finnal-arr:917729544

...

mid-before-arr:688237128

arr-count:191614

min-finnal-arr:688237320

...

mid-before-arr:289396784

arr-count:3340

min-finnal-arr:289396976

finnal-data:289396976

从结果可以看出,随着循环的进行,$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读取大数据处理,其实都是用时间换空间,哪种方式更适合,在实际生产中需要依据自身业务的特点去设计。

这是最近在项目中遇到的一些坑和解决方案的总结,如果有错误或更好地建议,欢迎指出,大家共同学习进步,感谢阅读。

文中涉及引用:php foreach循环中unset后续的键值问题

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180620G1W0UG00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券