本文目录导读:

针对PHP处理大数据量的流式读取,核心原则是:避免一次性将全部数据加载到内存,PHP脚本的内存限制(memory_limit)通常是瓶颈。
以下是针对不同场景的流式处理方案,从文件读取到数据库导出,以及HTTP响应:
流式读取大文件(CSV、TXT、日志)
a. 使用 fopen + fgets(逐行读取,最常用)
<?php
// 适合GB级日志文件
$handle = fopen('/path/to/huge_file.txt', 'r');
if ($handle) {
while (($line = fgets($handle)) !== false) {
// 处理每一行,例如解析、写入数据库、输出
processLine($line);
// 重要:如果处理很慢或内存增长,可以定期释放
if (memory_get_usage() > 100 * 1024 * 1024) { // 100MB
// 清理大变量,强制垃圾回收
$temp = null;
gc_collect_cycles();
}
}
fclose($handle);
}
?>
b. 使用 SplFileObject(面向对象迭代器)
<?php
$file = new SplFileObject('/path/to/huge.csv');
$file->setFlags(SplFileObject::READ_CSV); // CSV专用
// 或 SplFileObject::DROP_NEW_LINE
foreach ($file as $lineNumber => $row) {
// $row 是数组(CSV)或字符串(文本)
processRow($row);
}
?>
c. 分块读取二进制文件(图片、视频)
<?php
$handle = fopen('/path/to/large.mp4', 'rb');
$chunkSize = 8192; // 8KB
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
// 处理chunk,例如发送到客户端或写入另一个流
echo $chunk;
flush(); // 立即发送到浏览器
ob_flush();
}
fclose($handle);
?>
流式处理数据库查询(避免一次性加载所有记录)
a. 使用 PDO + unbuffered query(MySQL)
默认PDO会缓存所有结果到内存,必须关闭缓冲:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass', [
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, // 关键!
]);
$stmt = $pdo->prepare('SELECT * FROM huge_table');
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// 逐行处理,内存只保留一行
processRow($row);
}
?>
注意:未缓冲查询期间不能在同一连接上执行其他SQL,否则会报错。
b. 使用 mysqli + store_result(false)
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$query = 'SELECT * FROM huge_table';
$result = $mysqli->query($query, MYSQLI_USE_RESULT); // 不缓存结果
while ($row = $result->fetch_assoc()) {
processRow($row);
}
$result->free(); // 释放结果集
?>
流式输出到客户端(导出CSV/Excel/JSON)
当需要将大量数据实时发送给浏览器下载时,不能先存数组再echo。
CSV流式下载示例:
<?php
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="export.csv"');
// 打开输出流
$output = fopen('php://output', 'w');
行
fputcsv($output, ['ID', 'Name', 'Email']);
// 从数据库流式读取并逐行写入
$stmt = $pdo->prepare('SELECT * FROM users');
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($output, $row);
// 强制刷新到浏览器,适合长耗时导出
if (ob_get_level() > 0) {
ob_flush();
flush();
}
}
fclose($output);
exit;
?>
大型JSON数组流式输出(不生成完整JSON字符串)
<?php
header('Content-Type: application/json');
echo '['; // 启动数组
$first = true;
$stmt = $pdo->query('SELECT * FROM big_table');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
if (!$first) echo ','; // 逗号分隔
echo json_encode($row);
$first = false;
ob_flush(); flush();
}
echo ']'; // 结束数组
?>
内存管理高级技巧
| 问题 | 解决方案 |
|---|---|
| 处理每行时产生大量临时变量 | 使用 unset() + gc_collect_cycles() |
| 循环内拼接字符串导致内存暴增 | 改用 str_replace、sprintf 或直接输出到流 |
| 需要同时处理多个大文件 | 使用 Generator(yield)实现惰性加载 |
| 处理时间过长超时 | set_time_limit(0) 或分段处理配合前端轮询 |
使用生成器(Generator)实现内存友好的迭代器:
<?php
function readLargeFile($path) {
$handle = fopen($path, 'r');
while (!feof($handle)) {
yield fgets($handle); // 每次只产生一行
}
fclose($handle);
}
foreach (readLargeFile('/data/huge.log') as $line) {
processLine($line);
}
?>
性能对比与最佳实践
| 方法 | 内存占用 | 速度 | 适用场景 |
|---|---|---|---|
| file_get_contents + explode | ❌ 极高 | 快 | 小文件 (<10MB) |
| fopen + fgets | ✅ 极低 | 中等 | 任意大小文本文件 |
| PDO未缓冲查询 | ✅ 极低 | 快 | 数据库全表扫描 |
| Generator | ✅ 极低 | 中等 | 需要延迟计算/管道处理 |
| 分块fread + flush | ✅ 极低 | 取决于网络 | 大文件下载/代理 |
容易踩的坑
-
ob_start()导致内容缓存
流式输出前先关闭输出缓冲:ob_end_flush()或设置output_buffering = Off -
memory_limit设置过低
即使流式读取,某些框架或ORM会主动缓存,确保使用原生PDO/mysqli。 -
连接超时
对长时间运行的脚本设置:set_time_limit(0)配合前端轮询进度。 -
字符编码转换
流式处理大文件时编码转换(如GBK→UTF-8)会阻塞,考虑使用stream_filter_append或逐行转换。
总结推荐方案
- 大文件解析:
fopen+fgets或SplFileObject - 数据库导出:
PDO+MYSQL_ATTR_USE_BUFFERED_QUERY => false - 流式下载:
php://output+ob_flush()/flush() - 复杂数据处理管线:使用
Generator串联多个处理步骤
始终监控内存使用:memory_get_peak_usage(true),确保峰值不超过服务器可用内存的50%。