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秒。