【问题标题】:How to stream creation of a JSON File?如何流式创建 JSON 文件?
【发布时间】:2018-07-10 04:31:25
【问题描述】:

我正在尝试从数据库查询的大量转储中创建 JSON 文件,并且当我将 LIMIT 设置为 100000 行返回时工作,但是当我想要返回所有行时,它只会出现 502 错误(页面请求被取消,因为完成时间太长)。想知道是否有一种方法可以使用 php 简化 JSON 文件的创建过程,或者是否有一个库可以让我部分地构建 json 文件?

基本上我在这里运行一个 .php 文件来尝试从 woocommerce 获取所有 json 格式的订单,因为我购买的插件“CSV Import Suite”在导入订单时不起作用,它只是停留在队列中。

所以,我决定自己尝试导出所有订单,但一直出现 502 错误页面,它也永远不会创建 .json 文件,所以我想我需要一种方法来以某种方式流式传输它。对此的任何帮助将不胜感激......

ini_set('memory_limit', '-1');
ini_set('max_execution_time', '-1');
set_time_limit(0);
error_reporting(E_ALL);
ob_implicit_flush(TRUE);
ob_end_flush();

global $wpdb, $root_dir;

if (!defined('ABSPATH'))
    $root_dir = dirname(__FILE__) . '/';
else
    $root_dir = ABSPATH;


$download = isset($_GET['download']);

// Allows us to use WP functions in a .php file without 404 headers!
require_once($root_dir . 'wp-config.php');
$wp->init();
$wp->parse_request();
$wp->query_posts();
$wp->register_globals();

if (empty($download))
    $wp->send_headers();

// exclude
$exclude_post_statuses = array('trash', 'wc-refunded', 'wc_cancelled');


$start_date = !empty($_GET['start_date']) ? DateTime::createFromFormat('Y-m-d', $_GET['start_date']) : '';
$end_date = !empty($_GET['end_date']) ? DateTime::createFromFormat('Y-m-d', $_GET['end_date']) : '';


$order_db = array(
    'columns' => array(
        'p' => array('ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_excerpt', 'post_status', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order', 'post_type', 'post_mime_type', 'comment_count'),
        'pm' => array('meta_id', 'post_id', 'meta_key', 'meta_value'),
        'oi' => array('order_item_id', 'order_item_name', 'order_item_type', 'order_id'),
        'oim' => array('meta_id', 'order_item_id', 'meta_key', 'meta_value')
    )
);

$select_data = '';
$total_columns = count($order_db['columns']);
$i = 1;

foreach($order_db['columns'] as $column_key => $columns)
{
    $select_data .= implode(', ', array_map(
        function ($v, $k) { return $k . '.' . $v . ' AS ' . $k . '_' . $v; },
        $columns,
        array_fill(0, count($columns), $column_key)
    ));

    if ($i < $total_columns)
        $select_data .= ', ';

    $i++;
}

// HUGE DATABASE DUMP HERE, needs to be converted to JSON, after getting all columns of all tables...
$orders_query = $wpdb->get_results('
    SELECT ' . $select_data . '
    FROM ' . $wpdb->posts . ' AS p
    INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
    WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
    ORDER BY p.ID ASC', ARRAY_A);

$json = array();

if (!empty($orders_query))
{
    foreach($orders_query as $order_query)
    {
        if (!isset($json[$order_query['p_post_type']], $json[$order_query['p_post_type']][$order_query['p_post_name']]))
            $json[$order_query['p_post_type']][$order_query['p_post_name']] = array(
                'posts' => array(),
                'postmeta' => array(),
                'woocommerce_order_items' => array(),
                'woocommerce_order_itemmeta' => array()
            );

        if (!empty($order_query['p_ID']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['posts'][$order_query['p_ID']] = array_filter($order_query, function($k) {
                $is_p = strpos($k, 'p_');
                return $is_p !== FALSE && empty($is_p);
            }, ARRAY_FILTER_USE_KEY);

        if (!empty($order_query['pm_meta_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['postmeta'][$order_query['pm_meta_id']] = array_filter($order_query, function($k) {
                $is_pm = strpos($k, 'pm_');
                return $is_pm !== FALSE && empty($is_pm);
            }, ARRAY_FILTER_USE_KEY);

        if (!empty($order_query['oi_order_item_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['woocommerce_order_items'][$order_query['oi_order_item_id']] = array_filter($order_query, function($k) {
                $is_io = strpos($k, 'oi_');
                return $is_io !== FALSE && empty($is_io);
            }, ARRAY_FILTER_USE_KEY);


        if (!empty($order_query['oim_meta_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['woocommerce_order_itemmeta'][$order_query['oim_meta_id']] = array_filter($order_query, function($k) {
                $is_oim = strpos($k, 'oim_');
                return $is_oim !== FALSE && empty($is_oim);
            }, ARRAY_FILTER_USE_KEY);
    }
}

// Downloading or viewing?
if (!empty($download))
{
    // Outputs json in a textarea for you to copy and paste into a .json file for import...

    if (!empty($json))
    {
        $filename = uniqid('orders_') . '.json';

        $fp = fopen($filename, 'w');
        fwrite($fp, json_encode($json));
        fclose($fp);


        $size   = filesize($root_dir . '/' . $filename);
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header("Content-Disposition: attachment; filename=\"" . $filename . "\""); 
        header('Content-Transfer-Encoding: binary');
        header('Connection: Keep-Alive');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
        header('Content-Length: ' . $size);

        readfile($root_dir . '/' . $filename);
    }

}
else
{
    // Outputs json in a textarea for you to copy and paste into a .json file for import...
    if (!empty($json))
        echo '<textarea cols="200" rows="50">', json_encode($json), '</textarea>';
}

创建的 JSON 文件可能超过 500 MB,甚至可能高达 1 Gig 的数据。所以,我相信 PHP 在这里内存不足,需要以某种方式一点一点地处理,无论是在后台,还是完全不达到 php 内存限制。我相信内存限制设置为 1024 MB,这是相当高的,但还不够高,而且对于我正在做的事情,我认为我们永远不会有足够的内存来执行操作。我处理 json 和/或下载它的方式需要改变。而且我不想创建多个 json 文件,请只创建 1 个 JSON 文件。

【问题讨论】:

  • 我真的很好奇会消耗生成的json文件的软件!该软件真的需要1Gb数据来处理吗?我问这个是因为读取一个大的 json 文件是 a$$ 的痛苦,所以写它可能比这更痛苦。
  • 我不确定您一次检索所有数据的目的。为什么不将检索数据限制为 1000 行并创建单独的 JSON 文件?
  • 您为什么认为“流媒体”可以解决您的问题?这个词到底是什么意思?为什么不转储包含摘录的较小文件?这也会加快将文件读入其他系统的速度
  • 我会使用行分隔的 json (ldjson) 而不是完整的 json。这样每行都可以单独处理,非常适合流式传输。见en.wikipedia.org/wiki/Line_Delimited_JSON
  • 然而,没有人提供任何方法来流式创建 json 文件。太可悲了,我将不得不向提供仅调试问题的方法的人奖励积分....

标签: php json wordpress woocommerce


【解决方案1】:

我认为可能有几个问题。首先我建议你做一些分析。

    // HUGE DATABASE DUMP HERE, needs to be converted to JSON, after getting all columns of all tables...
    echo 'Start Time: '. date("Y-m-d H:i:s");
    echo ' Memory Usage: ' . (memory_get_usage()/1048576) . ' MB \n';

    $orders_query = $wpdb->get_results('
        SELECT ' . $select_data . '
        FROM ' . $wpdb->posts . ' AS p
        INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
        LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
        LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
        WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
        ORDER BY p.ID ASC', ARRAY_A);

    echo 'End Time: '. date("Y-m-d H:i:s");
    echo ' Memory Usage: ' . (memory_get_usage()/1048576) . ' MB \n';
    die('Finished');

    $json = array();

以上内容将帮助您了解目前使用了多少内存。如果它在打印“完成”之前失败,我们知道这不是 json 问题。如果脚本工作正常,那么我们可以先创建一个 csv 文件而不是 json。由于您正在运行选择查询,(此时)它不必是您需要的嵌套 json 文件。只需创建一个 CSV 文件即可实现扁平结构。

$csvFile = uniqid('orders') . '.csv';
$fp = fopen($csvFile, 'w');
if (!empty($orders_query))
{
    $firstRow = true;
    foreach($orders_query as $order_query)
    {
        if(true === $firstRow) {
            $keys = array_keys($order_query);
            fputcsv($fp, $order_query);
            $firstRow = false;
        }

        fputcsv($fp, $order_query);
    }
}
fclose($fp);

如果上述工作正常,您至少有一个 csv 文件可以使用。

此时我不确定您的数据结构嵌套有多复杂。例如,您拥有的 'p_post_type' 和 'p_post_name' 存在多少不同的值。您可能需要解析 csv 文件并为每个 ['p_post_type']['p_post_name']['posts']、['p_post_type']['p_post_name']['posts'] 创建多个 json 文件, ['p_post_type']['p_post_name']['woocommerce_order_items'] 和 ['p_post_type']['p_post_name']['woocommerce_order_itemmeta']

如果文件数量很少,您可以编写脚本自动合并它们或手动合并它们。如果您有太多嵌套项,则可能创建的 json 文件数量可能很多并且可能难以合并它们,并且可能不是一个可行的选择。

如果json文件的数量很多,我想知道有这么大的单个json文件的目的是什么。如果导出是一个问题,导入也可能是一个问题,尤其是在内存中摄取如此巨大的 json 文件。我相信如果创建 json 文件的目的是以某种形式导入它,那么在将来的某个阶段,我认为您可能不得不考虑只使用 csv 文件的选项,您可以使用它来过滤掉任何内容在那个时候需要。

我希望这会有所帮助。

进一步更新

在我看来,$wpdb->get_results 正在使用 mysqli_query/mysql_query(取决于您的配置)来获取结果。见wordpress query docs。以这种方式获取数据不是内存有效的方式。我相信您在这一点上可能会失败 ($wpdb->get_results) 本身。我建议您在不使用 $wpdb 的情况下运行查询。每当需要检索大数据时,就有一个无缓冲查询的概念,它对内存的影响非常小。更多信息可以在这里找到mysql unbuffering

即使您超过了这一点,您仍然会遇到内存问题,因为您将所有内容存储在 $json 变量中的方式正在消耗很多你的记忆。 $json 是一个数组,了解 PHP 数组的工作原理会很有趣。 PHP 数组是动态的,它们不会在每次添加新元素时分配额外的内存,因为那样会非常慢。相反,它将数组大小增加到 2 的幂,这意味着每当限制用尽时,它会将数组限制增加到其当前限制的两倍,并在此过程中尝试将内存增加到限制的两倍。这对 PHP 7 来说不是问题,因为他们对 php 核心进行了一些重大更改。因此,如果您有 2GB 的数据可能需要存储在 $json 中,则脚本可能会轻松分配 3-4 GB 内存,具体取决于它何时到达限制。更多细节可以在这里找到php arrayHow does PHP memory actually work

如果您考虑 $orders_query 的开销,它是一个数组与 $json 的开销相结合由于 PHP 数组的工作方式,它非常重要。

您也可以尝试创建另一个数据库 B。因此,当您从数据库 A 读取数据时,您同时开始向数据库 B 写入数据。最后,借助 MySQL 的强大功能,您拥有包含所有数据的数据库 B。您还可以将相同的数据推送到 MongoDB 中,这将是闪电般的速度,并且可能会帮助您处理您所追求的 json 嵌套。 MongoDB 旨在真正高效地处理大型数据集。

JSON 流解决方案

首先,我想说流媒体是顺序/线性过程。因此,它没有记忆在此时间点之前添加的内容或在此时间点之后添加的内容。它以小块的形式工作,这就是它如此高效的原因。因此,当您实际编写或阅读时,责任在于脚本,它维护特定的顺序,这有点说您正在编写/读取自己的 json,因为流式传输只理解文本并且不知道 json 是什么,并且不会打扰自己写/读正确的。

我在 github https://github.com/skolodyazhnyy/json-stream 上找到了一个库,它可以帮助你实现你想要的。我已经对代码进行了试验,我可以看到它会在您的代码中进行一些调整。

我要为你写一些伪代码。

//order is important in this query as streaming would require to maintain a proper order.
$query1 = select distinct p_post_type from ...YOUR QUERY... order by p_post_type;
$result1 = based on $query1; 

$filename = 'data.json';
$fh = fopen($filename, "w");
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);  

foreach($result1 as $fields1) {
    $posttype = $fields1['p_post_type'];
    $writer->enter($posttype, Writer::TYPE_ARRAY); 

    $query2 = select distinct p_post_name from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype order by p_post_type,p_post_name;
    $result2 = based on $query2;

    foreach($result2 as $fields2) {
        $postname = $fields1['p_post_name'];
        $writer->enter($postname, Writer::TYPE_ARRAY); 

        $query3 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where p_ID is not null order by p_ID;
        $result3 = based on $query3;
        foreach($result2 as $field3) {
            $writer->enter('posts', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field3);
        }
        $writer->leave(); 

        $query4 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where pm_meta_id is not null order by pm_meta_id;
        $result4 = based on $query4;
        foreach($result4 as $field4) {
            $writer->enter('postmeta', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field4);
        }
       $writer->leave(); 

        $query5 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where oi_order_item_id is not null order by oi_order_item_id;
        $result5 = based on $query5;
        foreach($result5 as $field5) {
            $writer->enter('woocommerce_order_items', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field5);
        }
        $writer->leave(); 

        $query6 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where oim_meta_id is not null order by oim_meta_id;
        $result6 = based on $query6;
        foreach($result6 as $field6) {
            $writer->enter('woocommerce_order_itemmeta', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field5);
        }
        $writer->leave(); 

    }
$writer->leave(); 
fclose($fh);

您可能必须开始将查询限制在 10 个左右,直到正确为止。由于上面的代码可能无法按原样工作。您应该能够以类似的方式阅读代码,因为同一个库有一个 Reader 类来提供帮助。我已经测试了读者和作家,他们似乎工作正常。

【讨论】:

  • 我从中导出的服务器无法为此操作分配足够的内存。但是,我要导入的服务器可以。无论如何,我只是简单地为每天的所有订单创建 json 文件,而且在某些情况下,我不得不将几天分成几个小时,以便查询工作。绝对是$wpdb-&gt;get_results 的问题,看来它消耗了太多内存。原始 MySQL 查询可能会更快,但是,仍然无法一次性导出,但有趣的是 phpMyAdmin 在很长一段时间后运行得很好......
  • 如何创建一个 CSV 文件比创建一个 JSON 文件更好?
  • 抱歉,我可能不清楚,但我想说的是,如果您使用原始 mysql unbuffered 查询,例如$mysqli-&gt;query("SELECT Name FROM City", MYSQLI_USE_RESULT);返回数据时它不会将所有结果存储在内存中,mysql_query 就是这种情况。它将按块顺序读取数据,在您的情况下特别有用,因为它更节省内存。如果您通过我提供的链接,他们已经给出了详细的解释。 phpMyAdmin 可能正在使用类似的东西,因为它似乎对你有用。
  • CSV 将数据流式传输到文件中,而不是将其存储在内存中,就像 $json 数组正在做的那样。
  • 好的,所以我猜这不能使用 JSON 格式来完成,应该重新考虑只使用 CSV 格式:(
【解决方案2】:

创建文件

您的代码的问题是您试图将整个数据集放入内存中,一旦您的数据库变得足够大,最终将失败。为了克服这个问题,您必须分批获取数据。

我们将多次生成查询,因此我将您的查询提取到一个函数中。为简洁起见,我跳过了传递所需参数(或将它们设为全局参数),因此您必须自己让它工作。

function generate_query($select, $limit = null, $offset = null) {
    $query = 'SELECT ' . $select . '
    FROM ' . $wpdb->posts . ' AS p
    INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
    WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
    ORDER BY p.ID ASC';

    if ($limit && $offset) {
        $query .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
    }

    return $query;
}

现在我们将分批从数据库中获取结果,我们定义批处理计数,即我们将加载到内存中的每次迭代的记录数。您可以稍后使用此值来找到一个足够快且不会导致 PHP 崩溃的值。请记住,我们希望尽可能减少数据库查询的数量:

define('BATCH_COUNT', 500);

在我们创建循环之前,我们需要知道我们将进行多少次迭代(数据库调用),因此我们需要总订单数。有了这个和批次计数,我们可以很容易地计算出这个值:

$orders_count = $wpdb->get_col(generate_query('COUNT(*)'));
$iteration_count = ceil($orders_count / BATCH_COUNT);

因此,我们希望在结果文件中包含一个巨大的 JSON 字符串。由于每次迭代都会有一个包含对象数组的单独 JSON,因此我们只需从 JSON 字符串的每一侧剥离 [] 并将该字符串放入文件中。

最终代码:

define('FILE', 'dump.json');
file_put_contents(FILE, '[');

for ($i = 0; $i < $iteration_count; $i++) {
    $offset = $i * BATCH_COUNT;

    $result = $wpdb->get_results(
        generate_query($select_data, BATCH_COUNT, $offset),
        ARRAY_A
    );

    // do additional work here, add missing arrays etc.
    // ...

    // I assume here the $result is a valid array ready for
    // creating JSON from it
    // we append the result file with partial JSON

    file_put_contents(FILE, trim(json_encode($result), '[]'), FILE_APPEND);
}

file_put_contents(FILE, ']', FILE_APPEND);

恭喜,你刚刚创建了你的第一个巨大的 JSON 转储;)你应该在命令行中运行这个脚本,这样它就可以得到它需要的时间,从现在开始不需要修改内存限制,因为我们希望永远不会达到极限。

发送文件

使用 PHP 流式传输大文件很容易,has already been answered 多次使用。但是我个人不建议你在 PHP 中做任何耗时的事情,因为它作为一个长时间运行的进程很糟糕,无论是在命令行中还是作为文件服务器。

我假设您使用的是 Apache。您应该考虑使用SendFile 并让 Apache 为您完成艰苦的工作。这种方法在处理大文件时效率更高。这个方法很简单,只需要在header中传递文件的路径即可:

header('X-Sendfile: ' . $path_to_the_file);

如果你使用 Nginx,还有XSendFile 支持。

此方法不占用大量内存,不会阻塞 PHP 进程。该文件也不需要在 webroot 中可访问。我一直使用 XSendFile 向经过身份验证的用户提供 4K 视频。

【讨论】:

  • 我不明白如何将此标头添加到 php 文件中会发生这种情况?您指出的答案甚至不使用X-Sendfile
  • 另外,我需要先创建文件,所以发送一个甚至没有创建的文件的路径,如何?
  • 那么问题是生成文件的过程由于PHP内存限制而中断?
  • 好的,这告诉了我问题,但是如何流式传输呢?解决办法是什么?
  • 我问了你一个问题,你没有回答。问题在于流这个词,我相信你的意思是别的。流式传输是通过线路以块的形式发送文件的过程,我为您写了一个解决该问题的方法。但是我开始认为真正的问题是您无法生成数据的转储文件。我说的对吗?
【解决方案3】:

首先,你应该问自己一个问题:我需要自己写数据库转储吗?

如果没有,那么您可以简单地使用一些可以为您完成工作的服务。 Mysqldump-php 应该可以胜任。

那么你可以简单地:

include_once(dirname(__FILE__) . '/mysqldump-php-2.0.0/src/Ifsnop/Mysqldump/Mysqldump.php');
$dump = new Ifsnop\Mysqldump\Mysqldump('mysql:host=localhost;dbname=testdb', 'username', 'password');
$dump->start('storage/work/dump.sql');

这应该创建.sql 文件。但是,您想要 json 文件。不过这应该不是问题。这个工具将完成剩下的工作:http://www.csvjson.com/sql2json

你也可以在github上找到sql2json的源码:https://github.com/martindrapeau/csvjson-app

【讨论】:

  • 数据库转储不起作用,我已经问过自己这个问题了。该数据库非常庞大,并且进行数据库转储并不能解决我正在优化的站点上的性能问题。我正在尝试减少数据库,并且只在删除一些插件等之后才获取需要的内容。这会导致帖子 ID 不对齐,我正在尝试减少这些,以便新的数据库自动增量列是更好地符合要求。 mysqldump 在这里没有帮助。
  • 所以,基本上我已经将其拆分为 10 年内每周创建一个 json 文件,这将创建 520 个 json 文件,我将在执行导入功能时循环这些文件,如果文件存在,并在导入完成后删除每个文件。不幸的是,这是我能想到的唯一方法。将需要多次调用导入文件,可能很多次,所以决定设置一个 CRON 来处理这个问题,而不是我重新加载页面几百次。
  • 我仍然不认为您正在做的是最佳解决方案。数据库可能很大,这就是我们拥有它们的原因。您是否尝试以其他方式解决性能问题?也许添加索引?这通常是提高数据库性能的最简单方法。如果您有足够的可用空间,它可能会产生巨大的影响。此外,您似乎正在使用 WordPress。我相信你可以优化 SQL 查询。
【解决方案4】:

我相信您可能正在寻找Generators http://php.net/manual/en/language.generators.overview.php https://scotch.io/tutorials/understanding-php-generators

您无需创建庞大的 $json 数组,而是遍历每个 $order_query 并在每次迭代中执行操作,从而无需将其存储在内存中。

【讨论】:

    【解决方案5】:

    您的问题是您从查询中得到一个大型结果集,这很重,因为您有 3 个联接。

    你可以定义一个limit并使用offset来获取chunks中的数据,然后分部分输出你的json。主要问题是以某种方式获取内存中的 json 数据,然后将其访问以部分输出。

    对于后者,可以使用 cache 或 nosql 数据库。我的解决方案将使用缓存,尤其是内存缓存:

    class Cache {
    
        private $cache;
    
        public function __construct($cache)
        {
            $this->cache = $cache;
        }
    
        public function addPostName($postName)
        {
            $this->addKeyToJsonObject('postNames', $postName);
        }
    
        public function addKeyToJsonObject($rootName, $key)
        {
            $childNames = $this->cache->get($rootName);
            if($childNames === false) {
                $this->cache->set($rootName, [$key]);
            }
            else {
                $childNamesList = $childNames;
                // not found
                if(array_search($key, $childNamesList) === false) {
                    $childNamesList[] = $key;
                    $this->cache->set($rootName, $childNamesList);
                }
            }
        }
    
        public function getPostNames()
        {
            return $this->cache->get('postNames');
        }
        public function set($key, $value) {
            $this->cache->add($key, $value);
        }
    
        public function addPostIdsByNameAndType($postName, $type, $pid)
        {
            $this->addKeyToJsonObject($postName . '-' . $type, $pid);
        }
    
        public function getPostIdsByNameAndType($postName, $type)
        {
            return $this->cache->get($postName . '-' . $type);
        }
    
        public function addPostValueByNameTypeAndId($postName, $type, $pid, $value)
        {
            $this->cache->set($postName . '-' . $type . '-' . $pid, $value);
        }
    
        public function getPostValueByNameTypeAndId($postName, $type, $pid)
        {
            return $this->cache->get($postName . '-' . $type . '-' . $pid);
        }
    }
    

    然后:

    $memcache = new Memcache();
    $memcache->connect('127.0.0.1', 11211) or die ("Could not connect");
    
    $memcache->flush();
    
    $cache = new Cache($memcache);
    
    header('Content-disposition: attachment; filename=file.json');
    header('Content-type: application/json');
    echo '{"shop_order":{';
    
    function getResultSet($wpdb, $offset = 1) {
        return $wpdb->get_results('
        SELECT ' . $select_data . '
    FROM ' . $wpdb->posts . ' AS p
    INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
    WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
    ORDER BY p.ID ASC LIMIT 1000 OFFSET ' . $offset, ARRAY_A);
    
    }
    $offset = 1;
    
    $orders_query = getResultSet($wpdb, 1);
    while(!empty($orders_query)) {
        cacheRowData($cache, $orders_query);
        $offset = $offset + 1000;
        $orders_query = getResultSet($wpdb, $offset);
    }
    
    outputRowData($cache);
    
    function cacheRowData($cache, $orders_query)
    {
        foreach($orders_query as $order_query) {
    
            if(empty($order_query)) { continue; }
            $cache->addPostName($order_query['p_post_name']);
    
            // posts
            if (!empty($order_query['p_ID'])) {
                $cache->addPostIdsByNameAndType($order_query['p_post_name'],'posts', $order_query['p_ID']);
    
                $value = array_filter($order_query, function($k) {
                    $is_p = strpos($k, 'p_');
                    return $is_p !== FALSE && empty($is_p);
                }, ARRAY_FILTER_USE_KEY);
                $cache->addPostValueByNameTypeAndId($order_query['p_post_name'],'posts', $order_query['p_ID'], $value);
            }
            if (!empty($order_query['pm_meta_id'])) {
            $cache->addPostIdsByNameAndType($order_query['p_post_name'],'postmeta', $order_query['pm_meta_id']);
    
            $value = array_filter($order_query, function($k) {
                $is_pm = strpos($k, 'pm_');
                return $is_pm !== FALSE && empty($is_pm);
            }, ARRAY_FILTER_USE_KEY);
            $cache->addPostValueByNameTypeAndId($order_query['p_post_name'],'postmeta', $order_query['pm_meta_id'], $value);
        }
            // here do the same for "woocommerce_order_items" and "woocommerce_order_itemmeta"
        }
    }
    
    function outputRowData($cache)
    {
    
        $cachedPostNames = $cache->getPostNames();
        $firstRow = true;
    
        foreach($cachedPostNames as $postName) {
    
            if(empty($postName)) { continue; }
    
            if($firstRow === false) {
                echo ',';
            }
            $firstRow = false;
    
            echo '"' . $postName . '":{';
            $postIds = $cache->getPostIdsByNameAndType($postName, 'posts');
            if(!$postIds) {
                $postIds = [];
            }
    
            // generate posts
            $postValues = [];
            foreach ($postIds as $postId) {
                $postValues[$postId] = $cache->getPostValueByNameTypeAndId($postName, 'posts', $postId);
            }
    
            $postMetaIds = $cache->getPostIdsByNameAndType($postName, 'postmeta');
            if(!$postMetaIds) {
                $postMetaIds = [];
            }
            $postMetaValues = [];
            foreach ($postMetaIds as $postMetaId) {
                $postMetaValues[$postMetaId] = $cache->getPostValueByNameTypeAndId($postName, 'postmeta', $postMetaId);
            }
            // here do the same for "woocommerce_order_items" and "woocommerce_order_itemmeta"
    
            echo '"posts":' . json_encode($postValues) . ',';
            echo '"postmeta":' . json_encode($postMetaValues);
            echo '}';
            ob_flush();
            flush();   // flush the output to start the download
        }
    }
    
    
    echo '}}';
    

    【讨论】:

    • 这看起来很有希望,会试一试,让你知道。不过,忘记在getResultSet 函数中提取$select_data 变量。
    • 抱歉,出现错误。我更新了代码,应该将缓存和输出代码分开。
    【解决方案6】:

    因此,您需要做很多事情才能做到这一点。我会记住我的所有观点。

    WebServer 终止

    如果您使用 Apache 或 Nginx/PHP-FPM,默认情况下,两者都对命中的 url 有超时。所以即使你用过

    ini_set('memory_limit', '-1');
    ini_set('max_execution_time', '-1');
    set_time_limit(0);
    

    要让脚本长时间运行,但是 Apache、Nginx、PHP-FPM 都有超时,这将不允许您的脚本工作。因此,您需要修复这些以使其正常工作。您从未提及您使用的服务器。但是 NGINX+PHP-FPM 在默认配置下肯定会导致 502。

    内存使用情况

    虽然你用过

    ini_set('memory_limit', '-1');
    

    如果您的内存需求上升,PHP 可能会开始使用分页,您的代码可能会变慢。

    PHP CLI 还是 PHP Web?

    不确定这里的执行频率是多少,但如果频率较低,您可以考虑通过 PHP-CLI 而不是 HTTP 运行数据转储脚本。这意味着您将直接通过终端运行 PHP 脚本以将 JSON 转储到文件中,然后使用 URL 直接下载文件

    使用 X-Sendfile 或 X-Accel-Redirect

    如果您使用的是 apache,您可以发送标头

    header('X-Sendfile: /data/generated.json');
    

    如果是 Nginx,你可以发送一个

    header('X-Accel-Redirect: /data/generated.json');
    

    只有在您决定将脚本作为 web 而不是作为 CLI 运行时,您才会这样做。 JSON 生成完成后,您不希望脚本读取文件和服务器。你只需要网络服务器来处理它。

    无缓冲查询而不是 WPDB 查询

    https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/wp-db.php#L2480

    默认情况下,WPDB 查询将所有数据提取到内存中。但是您可以使用无缓冲查询自己查询数据库,这不会淹没内存

    Example #1 Unbuffered query example: mysqli
    
    <?php
    $mysqli  = new mysqli("localhost", "my_user", "my_password", "world");
    $uresult = $mysqli->query("SELECT Name FROM City", MYSQLI_USE_RESULT);
    
    if ($uresult) {
       while ($row = $uresult->fetch_assoc()) {
           echo $row['Name'] . PHP_EOL;
       }
    }
    $uresult->close();
    ?>
    Example #2 Unbuffered query example: pdo_mysql
    
    <?php
    $pdo = new PDO("mysql:host=localhost;dbname=world", 'my_user', 'my_pass');
    $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
    
    $uresult = $pdo->query("SELECT Name FROM City");
    if ($uresult) {
       while ($row = $uresult->fetch(PDO::FETCH_ASSOC)) {
           echo $row['Name'] . PHP_EOL;
       }
    }
    ?>
    Example #3 Unbuffered query example: mysql
    
    <?php
    $conn = mysql_connect("localhost", "my_user", "my_pass");
    $db   = mysql_select_db("world");
    
    $uresult = mysql_unbuffered_query("SELECT Name FROM City");
    if ($uresult) {
       while ($row = mysql_fetch_assoc($uresult)) {
           echo $row['Name'] . PHP_EOL;
       }
    }
    ?>
    

    https://secure.php.net/manual/en/mysqlinfo.concepts.buffering.php

    PS:我现在脑子里可能还缺少一些点,很快就会更新

    【讨论】:

      猜你喜欢
      • 2013-04-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-09
      • 1970-01-01
      • 2021-12-11
      相关资源
      最近更新 更多