主页 M

yield在数据库查询中的内存优化探索

2023-08-17 网页编程网 网页编程网

1.背景

php处理大数据时,防止内存限制而卡死。

2.原理

yield函数,一条条数据在内存中处理,而不是一次性全取完,所以占内存少。

2.1单独读数据

以下实例是单条读数据。

function aa($number){
for($i=0;$i<$number;$i++){
$data[]= $i.'-'.time();
}
return $data;
}

$result = aa(5);
foreach($result as $value){
sleep(1);//这里停顿1秒
echo $value;
}
//以下是输出
0-1555485328
1-1555485328
2-1555485328
3-1555485328
4-1555485328

经过yield优化后这样:

function aa($number){
for($i=0;$i<$number;$i++){
yield $i.'-'.time();
}
}
$result = aa(5);
foreach($result as $value){
sleep(1);//这里停顿1秒
echo $value;
}
//以下输出
0-1555485647
1-1555485648
2-1555485649
3-1555485650
4-1555485651

程序会先从for里面拿出一个值,然后利用这个值来进行foreach,当这次循环完了后,再自动去for里面取第二个值。可以发现,只是时间变了,结果内容完全是一样的,像这样一个值一个值的处理,不管数据有多大,始终只处理一个值。就很大程度上优化了代码 ,节约了资源。以下演示读文本。

function read($path){
$file=fopen($path, "r");
//输出文本中所有的行,直到文件结束为止。
while(! feof($file)){
yield fgets($file).time();//fgets()函数从文件指针中读取一行
}
fclose($file);
}

$a = read('test.txt');
foreach ($a as $val){
sleep(1);
echo $val;
}

2.2优化内存实例

$start_mem = memory_get_usage();
$arr = range(1,10000);
foreach($arr as $item){
//echo $item;
}
$end_mem = memory_get_usage();
echo "use mem ".($end_mem - $start_mem).'bytes'.PHP_EOL;

占内存528440bytes,而若使用以下方法,内存仅为:32bytes,就是差这么大。

$start_mem = memory_get_usage();
function yield_range($start, $end){
while($start <= $end){
$start++;
yield $start;
}
}
foreach(yield_range(0, 9999) as $item ){
echo $item;
}
$end_mem = memory_get_usage();
echo "use mem".($end_mem - $start_mem).'bytes'.PHP_EOL;

yield返回的是一个叫做Generator生成器的object对象,而这个生成器是实现了Iterator接口,所以可以使用foreach进行迭代。当然,还有其他方法,如显示、修改当前值。

function yield_range($start,$end){
while($start <= $end){
$ret = yield $start;
$start++;
echo "yield receive".$ret.PHP_EOL;
}
}
$generator=yield_range(1,10);
$generator->send($generator->current()*10);

3.操作数据库

通过从数据库导出数据生成csv文件来说明。

3.1数据查询内存溢出

mysqli_query(connection,query,resultmode);通常都是直接填写第一第二个参数就直接查询,但该函数默认的是对全部结果集进行缓存,这会导致数据过多的时候,内存也会溢出。因此,需要设置第三个参数为MYSQLI_USE_RESULT来逐行读取结果集。那或许您有疑问为什么不直接遍历该mysqli_query()方法返回的结果来当每一行数据写到CSV中,而还要用yield来存储一次每行数据再写到CSV中呢?其实这是因为mysqli_query()返回的结果是mysqli_result Object形式,而fputcsv()这个方法要求第二个参数为数组。所以yield的作用就是中转站,但是他是一行行运输数据,而不是读多行来运输数据。

由于5.4.0 mysqli也支持Traversable,所以在mysqli_result中使用yield也没有意义。但是,以下仅是示范目的。

//1.普通查询
$stmt = $pdo->query('SELECT name FROM users');
foreach($stmt as $row){
var_export($row);
}


//2.yield查询,创建生成器
function mysqli_gen (mysqli_result $res){
while($row = mysqli_fetch_assoc($res)){
yield $row;
}
}
//这样查询防止过载
$res = $mysqli->query("SELECT * FROM users");
foreach (mysqli_gen($res) as $row){
var_export($row);
}

3.2 100w数据导出1个csv文件,在浏览器下载

/*
 * 该方法是把数据库读出的数据进行CSV文件输出,能接受百万级别的数据输出,因为用生成器,不用担心内存溢出。
 * @param string $sql 需要导出的数据SQL
 * @param string $mark 生成文件的名字前缀
 *
 */
function exportCsv($sql,$fileName){
//让程序一直运行
set_time_limit(0);
//设置程序运行内存
    ini_set('memory_limit', '128M');

    header('Content-Encoding: UTF-8');
    header("Content-type:application/vnd.ms-excel;charset=UTF-8");
    header('Content-Disposition: attachment;filename="' . $fileName . '.csv"');


    //打开php标准输出流
    $fp = fopen('php://output', 'a');


    //添加BOM头,以UTF8编码导出CSV文件,如果文件头未添加BOM头,打开会出现乱码。
    fwrite($fp, chr(0xEF).chr(0xBB).chr(0xBF));


    //添加导出标题
    fputcsv($fp, ['id', 'username', 'phone','city']);


    foreach (getExportData($sql) as $item) {
        //向csv表格中添加每一行数据
        fputcsv($fp, $item);
    }
    fclose($fp);  //每生成一个文件关闭
}//exportCsv

/**
 * 获取要导出的数据
 * 使用迭代生成器来缓存mysql查询结果,返回类型为数组
 * @param $sql
 * @return Generator
 */
function getExportData($sql){
    //连接数据库
    $con = mysqli_connect("localhost", "root", "");
    //连接数据库报错信息
    if (!$con) {
        die('Could not connect: ' . mysqli_error());
    }
    mysqli_select_db($con, "demo");
    mysqli_query($con,'set names utf8');
    //该处用MYSQLI_USE_RESULT 就是不缓存结果集中,也是为了避免内存溢出,相当于mysql_unbuffered_query
    foreach (mysqli_query($con, $sql,MYSQLI_USE_RESULT) as $row ){  //
        yield $row;
    }
    $con->close();
}//getExportData


//要导出的数据
$sql = 'SELECT id,username,phone,city FROM user where id < 1000000';
$mark = 'export';
exportCsv($sql,$mark);

3.3 100w数据导出多个csv文件,在文件夹中保存

/*
 * 该方法是把数据库读出的数据进行CSV文件输出,能接受百万级别的数据输出,因为用生成器,不用担心内存溢出。
 * @param string $sql 需要导出的数据SQL
 * @param string $mark 生成文件的名字前缀
 *
 */
function putCsv($sql, $mark){
    set_time_limit(0);
    $file_num = 0;  //文件名计数器
    $fileNameArr = array();
    $fp = fopen($mark .'_'.$file_num .'.csv', 'w'); //生成临时文件
    $fileNameArr[] = $mark .'_'.$file_num .'.csv';
    fwrite($fp, chr(0xEF).chr(0xBB).chr(0xBF));//转码,防止乱码
    //添加导出标题
    fputcsv($fp, ['id', 'type_id', 'state','addtime','usetime']);


    foreach (query($sql) as $a) {
        fputcsv($fp, $a);
    }
    fclose($fp);  //每生成一个文件关闭
}//putCsv


//生成器来缓存mysql查询结果,返回类型为数组
function query($sql){
    //连接数据库
    $con = mysqli_connect("localhost", "root", "");
    //连接数据库报错信息
    if (!$con) {
        die('Could not connect: ' . mysqli_error());
    }
    mysqli_select_db($con, "demo");
    mysqli_query($con,'set names utf8');
    //该处用MYSQLI_USE_RESULT 就是不缓存结果集中,也是为了避免内存溢出,相当于mysql_unbuffered_query
    foreach (mysqli_query($con, $sql,MYSQLI_USE_RESULT) as $row ){
        yield $row;
    }
    $con->close();
}//query


//数据库总数据
$count = 1000000;


//文件名称
$mark = 'test';
//一个excel保存的条数
$limit = 500000;
$page = 1;


$i = 0;
do{
    $i+= $limit;
    //计算偏移量
    $start = ($page-1)*$limit;
    $sql = "SELECT id,type_id,state,addtime,usetime FROM shop_ercode_records limit {$start}".','."{$limit}";
    putCsv($sql,$mark.$i);
    $page++;
} while ($i<$count);

3.4结论

浏览器只能同时下载一个文件,循环导出,浏览器最后智能下载最后一个文件。

导出的数据到csv能打开查看的只到100w行,如果超过100w行,进行导出多个csv文件。

使用for循环加缓冲区导出100w数据大概需要1分钟,而使用yield导出500w数据只需要3秒。

阅读原文
阅读 1364
123 显示电脑版