• 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 "
    start:" . memory_get_usage();
    $res = file_get_contents("./content.txt");     //文件里的是30W行整数
    $rs = explode("
    ", $res);         //这两段可当做是上面的大量的sql查询的结果
     
    echo "
    before-data:" . memory_get_usage();
    $num = count($rs);
    $sum = 0;
    foreach($rs as $k=>$value) {
    //计算实现逻辑
    $sum += $value;
    if($k % 50000 == 0 ){
      echo "
    count:".count($rs);
      echo "
    afterMemory:" . memory_get_usage();
    }
    unset($rs[$k]);
    }
    echo " finnal-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 "
    count:".count($rs);
            echo "
    afterMemory:" . 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 "
    count:".count($rs);
            echo "
    afterMemory:" . 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读取大数据处理,其实都是用时间换空间,哪种方式更适合,在实际生产中需要依据自身业务的特点去设计。
     
     

  • 相关阅读:
    URL参数加密专用
    错误
    js学习类
    .net第一个服务器控件
    javascript中的call()和apply()方法 原创实例
    FIS使用技巧
    自定义参数表单URL参数处理
    避免编写解决"不存在"问题的代码
    从 1.1.0 升级到 1.2.0 的注意事项
    jquery常用插件,应用解析
  • 原文地址:https://www.cnblogs.com/zyp221314/p/9230036.html
Copyright © 2020-2023  润新知