PHP写大文件(数据库正常,PHP内存溢出)

问题描述

通过PHP,执行了一段针对MYSQL查询,然后件数据查询的结果写入到一个csv文件中,由于数据量非常大,通过WEB访问会存在超时的问题,所以程序执行是命令行下执行的。

在执行了很久之后,通过MYSQL控制台:show processlist;命令,发现刚才执行的SQL已经完成了,但是csv文件,并没有生成。

问题排查

由于程序对容错性处理不是很好,所以执行没有看到任何的错误记录,只能通过PHP或者MYSQL的错误日志来排错

  1. 通过查看MYSQL错误日志文件: hostname.err文件,并没有找到任务的错误记录

  2. 发现PHP并没有记录PHP的执行日志,所以需要在php.ini中开启PHP的日志记录
    error_log = /usr/local/php/var/log/php.log

  3. 再次执行刚才的查询,在PHP的错误日志中发现:

PHP Fatal error:  Allowed memory size of 23907532800 bytes exhausted (tried to allocate 88 bytes) in /path/1.php on line 2

错误提示PHP内存溢出。

最终得到结论:是由于MYSQL返回的数据太大,超出了PHP最大内存限制,导致程序

尝试方案

既然是PHP内存溢出,那就考虑在PHP端如果优化处理

  • *** 程序是Thinkphp开发的,执行SQL调用的是Thinkphp自带的query方法,通过追踪源码发现 ***

    private function getResult()
    {
        //返回数据集
        $result        = $this->PDOStatement->fetchAll(PDO::FETCH_ASSOC);
        $this->numRows = count($result);
        return $result;
    }
    

    query最终调用了该方法,fetchAll查询的资源。一下子将所有的结果集赋给了$result变量。
    这种方式,显然有问题,因此优化该方法,使自己的query请求调用原生PDO,通过while,每次处理一行结果集写入到csv文件

    $res_source = $link->query($sql_info['sql']);
    $fp = fopen($csv_file, 'a');
    while ($row = $res_source->fetch(PDO::FETCH_ASSOC)) {
        fputcsv($fp, $row);
        $i++;
    }
    

    但是最终得到的结果,还是PHP内存溢出,没有成功。结果集让然是全部缓存到了内存中。

  • *** 通过控制PHP对MYSQL的数据读取,每次逐行读取,而不是一次全发返回结果集。***
    按照这个思路,通过网上查找资料,找到了一些说明资料,描述:

    这个问题在PHP的官方网站上叫缓冲查询和非缓冲查询(Buffered and Unbuffered queries)。PHP的查询缺省模式是缓冲模式。也就是说,查询数据结果会一次全部提取到内存里供PHP程序处理。这样给了PHP程序额外的功能,比如说,计算行数,将指针指向某一行等。更重要的是程序可以对数据集反复进行二次查询和过滤等操作。但这种缓冲查询模式的缺陷就是消耗内存,也就是用空间换速度。
    相对的,另外一种PHP查询模式是非缓冲查询,数据库服务器会一条一条的返回数据,而不是一次全部返回,这样的结果就是PHP程序消耗较少的内存,但却增加了数据库服务器的压力,因为数据库会一直等待PHP来取数据,一直到数据全部取完。

    最终找到了一个关于PHP操作MYSQL的原生函数:mysql_unbuffered_query,PHP官方手册这样介绍该函数:

    mysql_unbuffered_query — 向 MySQL 发送一条 SQL 查询,并不获取和缓存结果的行
    *** 说明 ***
    resource mysql_unbuffered_query ( string $query [, resource $link_identifier ] )
    mysql_unbuffered_query() 向 MySQL 发送一条 SQL 查询 query,但不像 mysql_query() 那样自动获取并缓存结果集。
    一方面,这在处理很大的结果集时会节省可观的内存。另一方面,可以在获取第一行后立即对结果集进行操作,而不用等到整个 SQL 语句都执行完毕。
    当使用多个数据库连接时,必须指定可选参数 link_identifier。
    *** Note: ***
    mysql_unbuffered_query() 的好处是有代价的:在 mysql_unbuffered_query() 返回的结果集之上不能使用 mysql_num_rows() 和 mysql_data_seek()。此外在向 MySQL 发送一条新的 SQL 查询之前,必须提取掉所有未缓存的 SQL 查询所产生的结果行。

    所以我们在读取大量数据的时候,可以使用mysql_unbuffered_query()来替代mysql_query()。设置为非缓冲查询,设置方法:
    修改PDO实例选项:

    <?php
    /*
     * Mysqli设置格式
     * 通过对query方法设置:MYSQLI_USE_RESULT(只是读取了结果集的元信息)
     * 另一种默认的方法是:MYSQLI_STORE_RESULT(把结果集从mysqlServer读到客户端)
     */
    $mysqli = new mysqli($db_hosts, $db_user, $db_pwd, $db_name); 
    $uresult = $mysqli->query("select * from table_name", MYSQLI_USE_RESULT); 
    
    /*
     * PDO设置格式
     * 通过对setAttribute,设置非缓冲查询
     */
    $pdo = new PDO($db_dns, $db_user, $db_pwd);
    $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
    
    /*
     * mysql设置格式
     * 通过调用mysql_unbuffered_query方法,来替换query方法
     */
    $connect = mysql_connect($db_hosts, $db_user, $db_pwd); 
    $db = mysql_select_db($db_name); 
    $result = mysql_unbuffered_query("select * from table_name"); 
    
    

    最终通过该方法,解决了PHP读大数据内存溢出,*** 最终成功写入CSV的文件的大小:388G,写入CSV文件行数:9亿多行 ***。