PHP项目怎样实现数据分段导出?

wen PHP项目 27

本文目录导读:

PHP项目怎样实现数据分段导出?

  1. 方案一:使用生成器 (Generator) + 流式输出 (CSV)
  2. 方案二:分文件导出 + 打包下载
  3. 方案三:异步任务 + 轮询下载(推荐用于生产)
  4. 性能对比与建议
  5. 注意事项

这是一个很实用的需求,在大数据量导出(如导出几万、几十万条记录到 Excel/CSV)时,直接一次性查询并写入,极易导致内存溢出脚本执行超时

实现数据分段导出的核心思想是:分批查询 + 分批写入 + 即时输出(或分文件)

以下是几种主流的实现方案和关键代码示例:

使用生成器 (Generator) + 流式输出 (CSV)

这是最常见、内存占用最低的方法,利用 PHP 生成器,每次只从数据库读取一行,并立即写入输出流。

适用场景: 导出 CSV 文件(文本格式),对内存要求极严苛。

<?php
// 1. 设置超时和内存(虽然为了安全,但好的代码不依赖大内存)
set_time_limit(0);
ini_set('memory_limit', '512M'); // 适当放大,但后面靠算法控制
// 2. 模拟数据库查询生成器
function fetchRowsInGenerator($offset, $limit) {
    // 假设是 PDO 连接
    global $pdo;
    $stmt = $pdo->prepare("SELECT id, name, email FROM users LIMIT :offset, :limit");
    $stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
    $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();
    // 使用 yield 逐行返回,而不是 fetchAll()
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        yield $row;
    }
}
// 3. 设置输出头,直接输出到浏览器
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="users_'.date('Ymd').'.csv"');
$output = fopen('php://output', 'w');
// 写入 BOM 头,解决 Excel 乱码
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, ['ID', '姓名', '邮箱']);
// 4. 分段导出逻辑
$pageSize = 1000; // 每页处理1000条
$offset = 0;
// 先获取总条数(或一直查直到结果为空)
$total = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
$exported = 0;
while ($exported < $total) {
    // 每一轮循环,只获取1000条
    $generator = fetchRowsInGenerator($offset, $pageSize);
    foreach ($generator as $row) {
        fputcsv($output, $row);
        $exported++;
    }
    $offset += $pageSize;
    // 可选:为浏览器刷新缓冲区,防止超时(对于大型导出很重要)
    ob_flush();
    flush();
    // 如果总数据量极大,可以考虑在这里做一次简单的睡眠,避免数据库压力过大
    // usleep(100000); // 0.1秒
}
fclose($output);
exit;
?>

优点: 内存占用恒定(只占一行数据的内存)。 缺点: 不适合 Excel (xlsx) 格式,因为 xlsx 是二进制结构。


分文件导出 + 打包下载

当需要导出为 Excel (.xlsx) 或数据量特别大(如 50万条)时,建议生成多个临时文件,最后打包成 ZIP。

适用场景: 对格式要求高(xlsx),或需要压缩传输。

<?php
set_time_limit(0);
ini_set('memory_limit', '1024M');
require_once 'vendor/autoload.php'; // PhpSpreadsheet
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
// 1. 创建临时文件夹
$tempDir = sys_get_temp_dir() . '/export_' . uniqid();
mkdir($tempDir, 0777, true);
// 2. 定义分段参数
$pageSize = 5000; // 每5000条一个文件
$page = 1;
$offset = 0;
// 假设获取总条数
$total = 150000;
$filePaths = [];
while ($offset < $total) {
    // 2.1 查询数据
    $data = $pdo->query("SELECT * FROM users LIMIT $offset, $pageSize")->fetchAll(PDO::FETCH_ASSOC);
    if (empty($data)) break;
    // 2.2 创建新 Excel 对象
    $spreadsheet = new Spreadsheet();
    $sheet = $spreadsheet->getActiveSheet();
    // 写入表头(只在第一个文件写,或都写)
    $sheet->fromArray(['ID', '姓名', '邮箱'], NULL, 'A1');
    // 写入数据
    $sheet->fromArray($data, NULL, 'A2');
    // 2.3 保存临时文件
    $filename = "users_part_{$page}.xlsx";
    $filePath = $tempDir . '/' . $filename;
    $writer = new Xlsx($spreadsheet);
    $writer->save($filePath);
    $filePaths[] = $filePath;
    // 清理当前 Excel 对象,释放内存
    $spreadsheet->disconnectWorksheets();
    unset($spreadsheet);
    $offset += $pageSize;
    $page++;
}
// 3. 打包所有分片为 ZIP
$zip = new ZipArchive();
$zipFile = $tempDir . '/all_users.zip';
$zip->open($zipFile, ZipArchive::CREATE);
foreach ($filePaths as $file) {
    $zip->addFile($file, basename($file));
}
$zip->close();
// 4. 输出 ZIP 文件给浏览器
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="users_export.zip"');
header('Content-Length: ' . filesize($zipFile));
readfile($zipFile);
// 5. 清理临时文件
array_map('unlink', $filePaths);
rmdir($tempDir);
exit;
?>

优点: 兼容 xlsx 格式;失败后只需重新导出失败的分片(理论上支持断点续传)。 缺点: 需要磁盘空间存临时文件;需要用户解压。


异步任务 + 轮询下载(推荐用于生产)

对于超大规模数据(几十万条以上),任何“即时下载”都可能因为浏览器等待而超时,最佳实践是把导出做成异步任务

流程图:

  1. 用户点击导出。
  2. 后端生成一个 task_id,将导出任务放入消息队列(或数据库任务表)。
  3. 后台进程(Cron / Supervisor)拉取任务,分批次将数据写入文件。
  4. 用户轮询 /status?task_id=xxx,直到任务状态变为 completed
  5. 用户通过 /download?task_id=xxx 下载最终文件。

关键代码片段(核心逻辑):

// 任务处理器(后台进程执行)
function processExportTask($taskId) {
    // 1. 更新任务状态为 'processing'
    updateTaskStatus($taskId, 'processing');
    $filePath = storage_path("exports/{$taskId}.csv");
    $handle = fopen($filePath, 'w');
    $offset = 0;
    $batchSize = 1000;
    while (true) {
        $data = fetchData($offset, $batchSize);
        if (empty($data)) break;
        foreach ($data as $row) {
            fputcsv($handle, $row);
        }
        $offset += $batchSize;
        // 更新进度(可选)
        updateTaskProgress($taskId, $offset);
    }
    fclose($handle);
    // 2. 更新任务状态为 'completed'
    updateTaskStatus($taskId, 'completed');
}

优点: 不影响用户体验(点击后关闭页面,后台继续);支持大文件;易于扩展。 缺点: 实现复杂,需要 Redis/数据库做任务管理。


性能对比与建议

方案 内存占用 响应速度 格式支持 开发成本 推荐数据量
生成器+CSV 极低 快(流式) CSV < 10万行
PhpSpreadsheet 分片 中等 慢(文件IO) XLSX 5-50万行
异步任务 延迟(邮件/URL) 任意 > 10万行

注意事项

  1. MySQL LIMIT 的性能问题:offset 非常大时(如 100万),LIMIT 1000000, 1000 会非常慢。优化方案:
    • 使用 游标分页SELECT * FROM users WHERE id > last_id ORDER BY id ASC LIMIT 1000,利用主键索引,速度极快。
    • 代码示例替换:
      function fetchRowsAfterId($lastId, $limit) {
          $stmt = $pdo->prepare("SELECT * FROM users WHERE id > ? ORDER BY id ASC LIMIT ?");
          $stmt->execute([$lastId, $limit]);
          foreach ($stmt as $row) yield $row;
      }
  2. 超时设置: 始终在脚本开头执行 set_time_limit(0);ini_set('memory_limit', '1024M');
  3. 防止重复/丢失数据: 如果导出过程中数据库有新数据插入,使用游标分页(基于ID)不会影响数据完整性,因为新数据的ID更大。

根据你的具体需求(数据量大小、是否需要 Excel 格式、系统并发量),选择最合适的方案,如果是刚刚开始,建议从方案一(生成器+CSV) 入手,最简洁且性能可靠。

抱歉,评论功能暂时关闭!