时间:2022-6-6来源:本站原创作者:佚名
哪里治白癜风 http://m.39.net/pf/bdfyy/
导语

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

情况分类

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

大文件的处理

DB读取大数据的处理

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

大文件的处理

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

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

大文件的处理核心思路是:逐行读取。

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

这里给大家推荐一篇文章,有讲到具体的实现方法。使用PHP读取和解析大文件实战

DB读取大数据的处理

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

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

大数组无法处理

如果是数据量太大无法从数据库中读取,请优化数据库配置或者优化你的语句,一般情况是建议优化SQL缩小查询范围,将数据分批进行处理,毕竟DB配置或机器硬件也不能无限优化的。

结论先行,DB读取大数据的处理核心思路是:变量用完及时销毁。

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

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

sql="yoursql";rs=DB-query(sql);data=array();foreach(rsasv){//yourcode}unset(rs);//销毁变量

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

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

项目实践项目背景

最近在开发的海外媒体绩效项目,其中有个计算模块,需根据各个平台的投放数据(注册数、CPA、净收金额等指标)计算每个员工当月的绩效得分,分数用于辅助打绩效。

遇到问题

以5月份为例,从各个统计后台同步到绩效后台的数据量大约有70W+条记录,最极端的情况是,所有的数据都是同一个人投放的,换句话说,计算这个员工的得分,我需要先从数据库读取这70W+条记录,然后在程序中进行逻辑计算。在这里就遇到了刚才描述的问题,计算某员工得分,取出的数据量较小的时候,循环顺利执行完毕并且销毁了变量,但当取出的数据量较大的时候,在循环内就已经挂了,执行不到循环处理完毕之后销毁变量。

调试检验

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

sql="yoursql";rs=DB-query(sql);data=array();foreach(rsask=v){//yourcodeunset(rs[k]);//销毁变量}unset(rs);//销毁变量

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

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

set_time_limit(0);ini_set(memory_limit,M);//视自身业务情况,这里临时分配足够内存去测试echo"\r\nbefore-sql:".memory_get_usage();sql="mysql";rs=M()-query(sql);data=array();echo"\r\nbefore-data:".memory_get_usage();foreach(rsask=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:before-data:mid-before-arr:Array(...[reg_num]=2[ad_fee_rmb]=3.17)arr-count:min-finnal-arr:mid-before-arr:Array(...[reg_num]=2[ad_fee_rmb]=3.17)arr-count:min-finnal-arr:...mid-before-arr:arr-count:min-finnal-arr:finnal-data:

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

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

问题解决

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

set_time_limit(0);ini_set(memory_limit,M);//视自身业务情况,这里临时分配足够内存去测试echo"\r\nbefore-sql:".memory_get_usage();sql="mysql";rs=M()-query(sql);data=array();echo"\r\nbefore-data:".memory_get_usage();//foreach(rsask=value){num=count(rs);for(k=0;knum;k++){value=rs[k];//计算实现逻辑...//员工数据初步汇总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();

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

before-sql:before-data:mid-before-arr:Array(...[reg_num]=2[ad_fee_rmb]=3.17)arr-count:min-finnal-arr:mid-before-arr:Array(...[reg_num]=2[ad_fee_rmb]=3.17)arr-count:min-finnal-arr:...mid-before-arr:arr-count:min-finnal-arr:...mid-before-arr:arr-count:min-finnal-arr:finnal-data:

从结果可以看出,随着循环的进行,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读取大数据处理,其实都是用时间换空间,哪种方式更适合,在实际生产中需要依据自身业务的特点去设计。这是最近在项目中遇到的一些坑和解决方案的总结,如果有错误或更好地建议,欢迎指出,大家共同学习进步,感谢阅读。

文中涉及引用:phpforeach循环中unset后续的键值问题          预览时标签不可点收录于话题#个上一篇下一篇
转载请注明原文网址:http://www.coolofsoul.com/hjpz/hjpz/24123.html
------分隔线----------------------------